From d361ba67a8724b02a1dce306309a89cf4b384881 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 30 Apr 2024 17:13:57 +0800 Subject: [PATCH 001/153] 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 adb4db65ed..cf8d823f82 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 5030dee227..90e6bc31cf 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 6257faac1f..fa5c921dfb 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 2217843b5e99335df02628fa81573087062822b3 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 25 Oct 2024 15:27:26 +0800 Subject: [PATCH 002/153] 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 cbf283e37d..15d44ed616 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 d51bde9c2dcebb7d99cccf64cc56b67df1483419 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 25 Oct 2024 15:32:30 +0800 Subject: [PATCH 003/153] 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 cf8d823f82..7b780b6724 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 34202b1453..0201c6914f 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 fa5c921dfb..e845a7f121 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 6d9c6c3d2e..58e8caeff0 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 ce144a8eb3..73054bdf5e 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 be1ec51a568e4bb832c44f33c339e74d364f3d1d Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 25 Oct 2024 17:31:54 +0800 Subject: [PATCH 004/153] 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 e845a7f121..586b76cf5b 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 58e8caeff0..54bc0c2176 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" "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 075e31ec57c8eef18b5c9bc1c8af88b528c1fe4a Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 25 Oct 2024 18:31:46 +0800 Subject: [PATCH 005/153] 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 7b780b6724..b709b27b8a 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 90e6bc31cf..fc322e3467 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 3671a38023a29073240ba2cc103da92686b582ea Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 30 Apr 2024 17:39:45 +0800 Subject: [PATCH 006/153] 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 b709b27b8a..4c526a4be5 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 fc322e3467..937b6fcb13 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 0201c6914f..e1ad73d8da 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 586b76cf5b..2ddcaee1b8 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 54bc0c2176..4c7eea0d31 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 da3a6ab52dc55709ef0bf58556bd1ee8b20a69a5 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 30 Apr 2024 19:25:06 +0800 Subject: [PATCH 007/153] 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 2ddcaee1b8..175e70558f 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 4c7eea0d31..741deefd3a 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 37536c7ad16ee81417a5eeafa4b0d852d48b9f63 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 30 Apr 2024 19:28:01 +0800 Subject: [PATCH 008/153] 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 175e70558f..6abbf78990 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 741deefd3a..381676826e 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 de762058cda3313d15fe59ae616d1a5cb0738d21 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Wed, 1 May 2024 02:58:27 +0800 Subject: [PATCH 009/153] 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 6abbf78990..de611eba04 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 381676826e..415c8240ce 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 0216b9c93b4796b6ae3ca99f1c95723b2de8fc27 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 25 Oct 2024 15:17:41 +0800 Subject: [PATCH 010/153] 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 4c526a4be5..97a99af5df 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 4abd9bad22697983a42392985cfb893cf55b6a0e Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 7 Nov 2024 15:07:26 +0800 Subject: [PATCH 011/153] 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 97a99af5df..38bbc92d41 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 e783439c54132f1bb3f084818288d1560bc467ab Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 7 Nov 2024 20:28:50 +0800 Subject: [PATCH 012/153] 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 38bbc92d41..8731c6b7ad 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 937b6fcb13..3e6ffc1c66 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 de611eba04..16fb81dedb 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 46e0a437a127b8f10d7bb1d8204453444592a204 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 27 Jun 2024 08:36:19 +0800 Subject: [PATCH 013/153] 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 0000000000..b11e38157c --- /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 0000000000..70827f2076 --- /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 0000000000..2d8c26f7a5 --- /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 c77c2300f1543edc3c1208114b96662dff77ab20 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 27 Jun 2024 08:41:53 +0800 Subject: [PATCH 014/153] 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 | 55 +++++++++++++++++++++++++++++++++++++++ chainio/blockbeat_test.go | 28 ++++++++++++++++++++ chainio/interface.go | 19 +++++++++++--- 3 files changed, 99 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 0000000000..5df1cad777 --- /dev/null +++ b/chainio/blockbeat.go @@ -0,0 +1,55 @@ +package chainio + +import ( + "fmt" + + "github.com/btcsuite/btclog/v2" + "github.com/lightningnetwork/lnd/build" + "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 = build.NewPrefixLog(logPrefix, clog) + + 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 0000000000..9326651b38 --- /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 70827f2076..03c09faf7c 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 08520b07da0a4ddc767450a2056c8e83327b5bfb Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 1 Nov 2024 06:15:34 +0800 Subject: [PATCH 015/153] 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 0000000000..d6900b8f9c --- /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 0000000000..c41138fd28 --- /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 0000000000..5677734e1d --- /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 0fa9406c91b5ba730b3922679867c9cf77d53e37 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 27 Jun 2024 08:43:26 +0800 Subject: [PATCH 016/153] 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 d6900b8f9c..244a3ac8f7 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 c41138fd28..88044c0201 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" "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 029aa3181f5c78c4a8a9ef6108eebc3a24190fc6 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 17 Oct 2024 10:58:59 +0800 Subject: [PATCH 017/153] 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 0000000000..a9ec25745b --- /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 0000000000..3ef79b61b4 --- /dev/null +++ b/chainio/consumer_test.go @@ -0,0 +1,202 @@ +package chainio + +import ( + "testing" + "time" + + "github.com/lightningnetwork/lnd/fn" + "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 26e034a28764d409e7fe5cd35fc0b074efc2ae21 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 29 Oct 2024 21:23:29 +0800 Subject: [PATCH 018/153] 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 d7d10ba252..e7c7fa9a91 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/channeldb/models" @@ -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 { @@ -1365,3 +1378,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 ffa4a5d6e2..61f018a1aa 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/channeldb/models" "github.com/lightningnetwork/lnd/fn" @@ -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 { @@ -3104,6 +3117,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 8731c6b7ad..bcc1c642d8 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" "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 16fb81dedb..7df0381613 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" "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 a00120f2ce2290f6c65e27bf1487af20ba97149f Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 4 Jun 2024 20:31:03 +0800 Subject: [PATCH 019/153] 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 bcc1c642d8..a4e9428c85 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 7df0381613..a35947d860 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 36ef0c3806a62b4d7b48991956294f9ed363242b Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Mon, 18 Nov 2024 11:09:21 +0800 Subject: [PATCH 020/153] 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 a35947d860..1207b984aa 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 bb78060b7b8ee37b72f647b304b8db428e10b571 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 4 Jun 2024 20:53:33 +0800 Subject: [PATCH 021/153] 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 4b47a34294..50d1cea829 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 f2b43b0f80..e056a471d3 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 4c9d2b200b..e1a5adb1ad 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 b9182500bb..29767db131 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 e7ab421691..9a2c5d4f0c 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 47be71d3ec..1c26fc3aa7 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 58153f742a91987ec62b16d62ac2eea1d3861023 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 29 Oct 2024 21:48:13 +0800 Subject: [PATCH 022/153] 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 bd4ad85683..2001431c79 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 e7c7fa9a91..c5db626317 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 @@ -801,18 +804,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 @@ -820,94 +816,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: @@ -916,6 +840,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 abaca5c2ba..cb037e09ae 100644 --- a/contractcourt/chain_arbitrator_test.go +++ b/contractcourt/chain_arbitrator_test.go @@ -83,7 +83,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 { @@ -168,7 +167,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 981e9c25590eb8df8070d21862559eb2825ce611 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 29 Oct 2024 21:48:58 +0800 Subject: [PATCH 023/153] 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 61f018a1aa..c05700f4b6 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), @@ -2742,31 +2736,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 } @@ -3117,6 +3101,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 ac5253787d..5569d7af48 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/channeldb/models" @@ -226,6 +227,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) { @@ -1036,7 +1046,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") @@ -1074,7 +1084,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. @@ -1914,7 +1929,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. @@ -2026,8 +2042,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) }) } } @@ -2098,13 +2113,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, @@ -2472,7 +2489,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) @@ -2635,7 +2652,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) @@ -2783,7 +2800,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) @@ -2950,10 +2968,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], ) @@ -3133,3 +3155,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 e3167ca51b2b3acee4df82278878e5590f4f4330 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Wed, 5 Jun 2024 00:56:39 +0800 Subject: [PATCH 024/153] 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 b4d6877202..09c325f1bf 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 740b4471d5..63395651cc 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 c05700f4b6..7068952ce5 100644 --- a/contractcourt/channel_arbitrator.go +++ b/contractcourt/channel_arbitrator.go @@ -804,7 +804,7 @@ func (c *ChannelArbitrator) relaunchResolvers(commitSet *CommitSet, // TODO(roasbeef): this isn't re-launched? } - c.launchResolvers(unresolvedContracts, true) + c.launchResolvers(unresolvedContracts) return nil } @@ -1343,7 +1343,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 @@ -1567,16 +1567,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) } } @@ -2548,9 +2546,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", @@ -2571,7 +2567,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 50d1cea829..90daae1c3b 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 e056a471d3..15c92344cc 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 691822610a..cdf6a76c32 100644 --- a/contractcourt/contract_resolver.go +++ b/contractcourt/contract_resolver.go @@ -43,7 +43,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 6bda4e398b..2addc91c11 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 55d93a6fb3..649f0adf33 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 2466544c98..874d26ab9c 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 f67c34ff4e..e4a3aaee0d 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 e1a5adb1ad..39adae88fe 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 29767db131..b962a55a6e 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 9a2c5d4f0c..fbca316cfd 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 1c26fc3aa7..63d0cf7d5c 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 a24ca9e303818e0c653339818e1a87aad5f687cb Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 29 Oct 2024 22:01:16 +0800 Subject: [PATCH 025/153] 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 c5db626317..9978daab06 100644 --- a/contractcourt/chain_arbitrator.go +++ b/contractcourt/chain_arbitrator.go @@ -798,7 +798,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 } @@ -1215,7 +1215,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 7068952ce5..b224d36021 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() // If the channel has been marked pending close in the database, and we // haven't transitioned the state machine to StateContractClosed (or a diff --git a/contractcourt/channel_arbitrator_test.go b/contractcourt/channel_arbitrator_test.go index 5569d7af48..0ced525dca 100644 --- a/contractcourt/channel_arbitrator_test.go +++ b/contractcourt/channel_arbitrator_test.go @@ -295,7 +295,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 } @@ -522,7 +523,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() { @@ -580,7 +582,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() @@ -633,7 +636,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() @@ -745,7 +749,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() { @@ -872,7 +877,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() @@ -1153,7 +1159,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() @@ -1260,7 +1267,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() @@ -1366,7 +1374,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) } @@ -1484,7 +1493,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) } @@ -1671,7 +1681,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) } @@ -1755,7 +1766,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) } @@ -1785,7 +1797,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() @@ -1883,9 +1896,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 @@ -2079,7 +2093,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() { @@ -2113,7 +2128,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) @@ -2234,8 +2249,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) } @@ -2786,7 +2801,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() { @@ -2799,8 +2816,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) @@ -3076,7 +3092,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 952869cd8583ae8e08d0ec13bb1640da44c6bcec Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 29 Oct 2024 22:02:13 +0800 Subject: [PATCH 026/153] 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 9978daab06..9899044df9 100644 --- a/contractcourt/chain_arbitrator.go +++ b/contractcourt/chain_arbitrator.go @@ -574,13 +574,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 cb037e09ae..a6b60a9a21 100644 --- a/contractcourt/chain_arbitrator_test.go +++ b/contractcourt/chain_arbitrator_test.go @@ -96,7 +96,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() { @@ -183,7 +184,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 ec19d0913e..17568aad8c 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" @@ -2066,6 +2067,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 @@ -2160,13 +2167,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 } @@ -2211,7 +2218,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 } @@ -5134,3 +5141,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 a4e9428c85..115ae15bdf 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 1207b984aa..147a56ed8c 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 bb42ff5ec7ca268a827c5d001a5f1dd19c762013 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 17 Oct 2024 09:58:48 +0800 Subject: [PATCH 027/153] 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 17568aad8c..02cbfc583b 100644 --- a/server.go +++ b/server.go @@ -655,6 +655,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 @@ -2061,6 +2072,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. @@ -2126,12 +2160,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 1292c9bdf41f14f1a2a9c78eb2432a00156602ec Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 17 Oct 2024 10:14:00 +0800 Subject: [PATCH 028/153] 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 343d86f38e..7fa78bb142 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" @@ -194,6 +195,7 @@ func SetupLoggers(root *build.SubLoggerManager, interceptor signal.Interceptor) AddSubLogger( root, blindedpath.Subsystem, interceptor, blindedpath.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 02cbfc583b..824fd3279f 100644 --- a/server.go +++ b/server.go @@ -350,6 +350,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 @@ -613,6 +617,9 @@ func newServer(cfg *Config, listenAddrs []net.Addr, readPool: readPool, chansToRestore: chansToRestore, + blockbeatDispatcher: chainio.NewBlockbeatDispatcher( + cc.ChainNotifier, + ), channelNotifier: channelnotifier.New( dbs.ChanStateDB.ChannelStateDB(), ), @@ -1818,6 +1825,9 @@ func newServer(cfg *Config, listenAddrs []net.Addr, } s.connMgr = cmgr + // Finally, register the subsystems in blockbeat. + s.registerBlockConsumers() + return s, nil } @@ -1850,6 +1860,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. @@ -2487,6 +2516,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) @@ -2511,6 +2551,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 ac409a08915ecae107a273f03f3f7f9e241460f7 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Wed, 30 Oct 2024 04:38:27 +0800 Subject: [PATCH 029/153] 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 9899044df9..71a4545707 100644 --- a/contractcourt/chain_arbitrator.go +++ b/contractcourt/chain_arbitrator.go @@ -587,137 +587,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:lll - 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 @@ -769,7 +649,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 { @@ -1336,3 +1216,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:lll + 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 90daae1c3b..47ad4b8105 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 4005e2a07a651cd4e0efaf0e72d0db1d4b9fe594 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Wed, 22 May 2024 16:51:36 +0800 Subject: [PATCH 030/153] 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 b224d36021..9afb8c149b 100644 --- a/contractcourt/channel_arbitrator.go +++ b/contractcourt/channel_arbitrator.go @@ -1598,8 +1598,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, @@ -2795,14 +2795,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. @@ -2830,6 +2828,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 53fa893553..c904f21d1b 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 b7b4d33a8b..a920699a7b 100644 --- a/contractcourt/utxonursery.go +++ b/contractcourt/utxonursery.go @@ -793,7 +793,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 cbc2a16dae..cc345e461b 100644 --- a/htlcswitch/switch.go +++ b/htlcswitch/switch.go @@ -1605,7 +1605,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 a9018437d5..a236894e09 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 147a56ed8c..500fcdc2df 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 5b3b60ce63ec9a0124ce259c0e0f2210f7ceb27f Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 19 Nov 2024 17:34:09 +0800 Subject: [PATCH 031/153] 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 244a3ac8f7..87bc21fbaa 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 660fa82d1c3371e5bf98302390b52dbef57f1410 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 20 Jun 2024 21:56:52 +0800 Subject: [PATCH 032/153] contractcourt: add verbose logging in resolvers We now put the outpoint in the resolvers's logging so it's easier to debug. Also use the short channel ID whenever possible to shorten the log line length. --- 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 09c325f1bf..57fc834a3d 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 63395651cc..9a5f4bbe08 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 47ad4b8105..a146357fc8 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 cdf6a76c32..40e0b7c962 100644 --- a/contractcourt/contract_resolver.go +++ b/contractcourt/contract_resolver.go @@ -121,8 +121,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 = build.NewPrefixLog(logPrefix, log) } diff --git a/contractcourt/htlc_success_resolver.go b/contractcourt/htlc_success_resolver.go index 39adae88fe..ab159c0ad6 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 fbca316cfd..ecd0ce5b66 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 cd1a184bb0f4675a2c239c1adcaa2b63c7246608 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 14 Nov 2024 02:28:56 +0800 Subject: [PATCH 033/153] 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 ab159c0ad6..14519dee87 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:lll 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 ecd0ce5b66..be6b47c243 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 91cd342a00c55b632d105d94a99fb70a5219b15a Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 14 Nov 2024 21:59:16 +0800 Subject: [PATCH 034/153] 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 14519dee87..f5d3ec47ee 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:lll - secondLevelInput = input.MakeHtlcSecondLevelSuccessTaprootInput( - h.htlcResolution.SignedSuccessTx, - h.htlcResolution.SignDetails, h.htlcResolution.Preimage, - h.broadcastHeight, - input.WithResolutionBlob( - h.htlcResolution.ResolutionBlob, - ), - ) - } else { - //nolint:lll - 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 a5c05aeafb5dcfce876d5a871c1a68b831201018 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 14 Nov 2024 21:59:55 +0800 Subject: [PATCH 035/153] 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 f5d3ec47ee..545ecd2213 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 second-level success transition 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 be6b47c243..9223a2a39b 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 f1a9e6dba3f95c14f31cfc64791eb40c2a585547 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 14 Nov 2024 22:07:32 +0800 Subject: [PATCH 036/153] 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 874d26ab9c..aa2a635d68 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 9223a2a39b..1a9b045649 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 982470c091b76c38aface4b0ab53cc002b6f620f Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 14 Nov 2024 22:08:00 +0800 Subject: [PATCH 037/153] 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 | 249 ++++++++++++++----------- 1 file changed, 139 insertions(+), 110 deletions(-) diff --git a/contractcourt/htlc_timeout_resolver.go b/contractcourt/htlc_timeout_resolver.go index 1a9b045649..5cde5bba8f 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, ) - // 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 "+ + h.log.Infof("%T(%x): offering 2nd-level HTLC timeout tx to sweeper "+ "with deadline=%v, budget=%v", h, h.htlc.RHash[:], h.incomingHTLCExpiryHeight, budget) + // For an outgoing HTLC, it must be swept before the RefundTimeout of + // its incoming HTLC is reached. _, 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:lll - 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 dda820853877b30b5d071d6042a477b4b97ccb81 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 16 Jul 2024 08:44:53 +0800 Subject: [PATCH 038/153] 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 | 183 ++++++++++++------------- 1 file changed, 86 insertions(+), 97 deletions(-) diff --git a/contractcourt/htlc_timeout_resolver.go b/contractcourt/htlc_timeout_resolver.go index 5cde5bba8f..cb509c4eb3 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,80 @@ 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) + + // For the direct-timeout spend, we will jump to this checkpoint + // without calling `checkpointStageOne`. Thus we need to send the clean + // up msg to fail the incoming HTLC. + if h.isRemoteCommitOutput() { + failureMsg := &lnwire.FailPermanentChannelFailure{} + err := h.DeliverResolutionMsg(ResolutionMsg{ + SourceChan: h.ShortChanID, + HtlcIndex: h.htlc.HtlcIndex, + Failure: failureMsg, + }) + if err != nil { + return err + } + } + + // Create a resolver report for claiming of the htlc itself. + 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 fab3acf6d5d34f8672ac2816ba59b0a4fc8108c0 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 16 Jul 2024 08:53:00 +0800 Subject: [PATCH 039/153] 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 | 190 ++++++++++++++----------- 1 file changed, 110 insertions(+), 80 deletions(-) diff --git a/contractcourt/htlc_timeout_resolver.go b/contractcourt/htlc_timeout_resolver.go index cb509c4eb3..3c501a7fc8 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. // @@ -1306,3 +1231,108 @@ 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) + } + + // 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 0d5908e738d679ebd7d3903a86e3dbd252186e97 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Mon, 24 Jun 2024 21:49:21 +0800 Subject: [PATCH 040/153] 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 | 119 ++++++++++++++++++----------- contractcourt/breach_resolver.go | 14 ++++ contractcourt/contract_resolver.go | 10 +++ 3 files changed, 100 insertions(+), 43 deletions(-) diff --git a/contractcourt/anchor_resolver.go b/contractcourt/anchor_resolver.go index 57fc834a3d..ce360d38e3 100644 --- a/contractcourt/anchor_resolver.go +++ b/contractcourt/anchor_resolver.go @@ -84,49 +84,12 @@ 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. 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 +98,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 +124,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 +146,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 +184,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 9a5f4bbe08..75944fa6f7 100644 --- a/contractcourt/breach_resolver.go +++ b/contractcourt/breach_resolver.go @@ -83,6 +83,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 +124,16 @@ func newBreachResolverFromReader(r io.Reader, resCfg ResolverConfig) ( // A compile time assertion to ensure breachResolver meets the ContractResolver // interface. var _ ContractResolver = (*breachResolver)(nil) + +// 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 40e0b7c962..6d51f25b58 100644 --- a/contractcourt/contract_resolver.go +++ b/contractcourt/contract_resolver.go @@ -11,6 +11,7 @@ import ( "github.com/lightningnetwork/lnd/build" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/fn" + "github.com/lightningnetwork/lnd/sweep" ) var ( @@ -110,6 +111,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 e75f396b3d7c3d55764edf1b07434237106330e9 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 20 Jun 2024 22:05:33 +0800 Subject: [PATCH 041/153] 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 a146357fc8..0d2aa44304 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 15c92344cc..03b424c34c 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 aa7fcd0b91f519ea3fce0051b268ed8e22797667 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 16 Jul 2024 07:24:45 +0800 Subject: [PATCH 042/153] 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 +++++++-- contractcourt/mock_registry_test.go | 5 + 3 files changed, 157 insertions(+), 121 deletions(-) diff --git a/contractcourt/htlc_success_resolver.go b/contractcourt/htlc_success_resolver.go index 545ecd2213..b9d77051a6 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/channeldb/models" "github.com/lightningnetwork/lnd/fn" @@ -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 b962a55a6e..16de3e4a86 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:lll 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 } diff --git a/contractcourt/mock_registry_test.go b/contractcourt/mock_registry_test.go index 5c75185623..1af857b4b2 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, From 9ade1ef1bf4efba16b4531dbc0eea9459c17b5e0 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 16 Jul 2024 09:05:47 +0800 Subject: [PATCH 043/153] 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 0ced525dca..5c74ada39c 100644 --- a/contractcourt/channel_arbitrator_test.go +++ b/contractcourt/channel_arbitrator_test.go @@ -981,6 +981,7 @@ func TestChannelArbitratorLocalForceClosePendingHtlc(t *testing.T) { }, }, } + closeTxid := closeTx.TxHash() htlcOp := wire.OutPoint{ Hash: closeTx.TxHash(), @@ -1107,7 +1108,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 3c501a7fc8..bdac9e035d 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 @@ -1300,9 +1237,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 @@ -1336,3 +1275,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 63d0cf7d5c..18c90f8c65 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:lll 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:lll +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:lll 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 cea75bff2fb79c3a2eff55452e3fb7a2af6fcec5 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sun, 17 Nov 2024 10:48:16 +0800 Subject: [PATCH 044/153] 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. --- invoices/invoiceregistry.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/invoices/invoiceregistry.go b/invoices/invoiceregistry.go index 9d54b6ad8d..64b1b31908 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 566eb80766a0b7c9580e7a59f3dc9a877dbc4776 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sun, 17 Nov 2024 10:47:23 +0800 Subject: [PATCH 045/153] 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 2addc91c11..81bb0ce0a2 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:lll - 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 } @@ -312,6 +284,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()) @@ -369,7 +344,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 } @@ -388,7 +365,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 } @@ -435,6 +415,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:lll + 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:lll + 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. @@ -461,6 +511,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) } @@ -560,3 +612,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 649f0adf33..9b3204f2ee 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/channeldb/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 f4e8e755416fe9fca5b4da1da174ff9a91a8b239 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 20 Jun 2024 22:11:47 +0800 Subject: [PATCH 046/153] 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 aa2a635d68..0712e421d7 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 e4a3aaee0d..18b4486c5e 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 600732ef7d0a1300abf3679bbedf4c8e5e393e18 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Wed, 10 Jul 2024 18:08:23 +0800 Subject: [PATCH 047/153] 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 ce360d38e3..00e0eb4c47 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. @@ -87,7 +84,7 @@ func (c *anchorResolver) ResolverKey() []byte { // Resolve waits for the output to be swept. 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 } @@ -137,7 +134,7 @@ func (c *anchorResolver) Resolve() (ContractResolver, error) { ) c.reportLock.Unlock() - c.resolved = true + c.markResolved() return nil, c.PutResolverReport(nil, report) } @@ -152,14 +149,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. // @@ -196,7 +185,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 75944fa6f7..a89ccf7e23 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 @@ -60,7 +57,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) } @@ -73,8 +70,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: } @@ -87,19 +85,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 @@ -112,9 +104,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 0f44db2abb..ee6e24591e 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 0d2aa44304..f3abfbce7c 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 6d51f25b58..597c649e2f 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" @@ -120,6 +121,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. @@ -138,6 +142,19 @@ func (r *contractResolverKit) initLogger(prefix string) { r.log = build.NewPrefixLog(logPrefix, log) } +// 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 81bb0ce0a2..c59656de1d 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 @@ -395,7 +395,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 @@ -516,14 +517,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 0712e421d7..49041a223b 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 b9d77051a6..1184409993 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 16de3e4a86..b92fcb678f 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 bdac9e035d..b82d61741d 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 } @@ -1164,7 +1159,7 @@ func (h *htlcTimeoutResolver) checkpointClaim( } // Finally, we checkpoint the resolver with our report(s). - h.resolved = true + h.markResolved() return h.Checkpoint(h, report) } @@ -1289,7 +1284,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 18c90f8c65..3f2f87228e 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 0d1e81b1149cc72d8bfa2ddc81453f0a71966b6a Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 11 Jul 2024 16:19:01 +0800 Subject: [PATCH 048/153] 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 00e0eb4c47..c192aeb88d 100644 --- a/contractcourt/anchor_resolver.go +++ b/contractcourt/anchor_resolver.go @@ -176,13 +176,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 a89ccf7e23..5644e60fad 100644 --- a/contractcourt/breach_resolver.go +++ b/contractcourt/breach_resolver.go @@ -123,13 +123,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 f3abfbce7c..fd9826ceed 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 597c649e2f..c608882d4e 100644 --- a/contractcourt/contract_resolver.go +++ b/contractcourt/contract_resolver.go @@ -119,8 +119,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 @@ -155,6 +158,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 c59656de1d..5379746408 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 49041a223b..fe8419a59e 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 1184409993..98c2435f07 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 b82d61741d..11eb6f1a73 100644 --- a/contractcourt/htlc_timeout_resolver.go +++ b/contractcourt/htlc_timeout_resolver.go @@ -1274,13 +1274,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.launched.Store(true) switch { // If we're already resolved, then we can exit early. From aeafa63cf75bccc11d1d3c0f983a822225d29663 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Wed, 26 Jun 2024 07:41:51 +0800 Subject: [PATCH 049/153] 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 9afb8c149b..2b3551e75f 100644 --- a/contractcourt/channel_arbitrator.go +++ b/contractcourt/channel_arbitrator.go @@ -804,7 +804,7 @@ func (c *ChannelArbitrator) relaunchResolvers(commitSet *CommitSet, // TODO(roasbeef): this isn't re-launched? } - c.launchResolvers(unresolvedContracts) + c.resolveContracts(unresolvedContracts) return nil } @@ -1343,7 +1343,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 @@ -1566,18 +1566,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 @@ -2616,6 +2673,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(): @@ -3117,6 +3181,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 5c74ada39c..681a92f2bb 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" @@ -1091,12 +1092,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. @@ -1139,8 +1135,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) @@ -2820,27 +2820,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, } @@ -2925,7 +2926,9 @@ func TestChannelArbitratorAnchors(t *testing.T) { //nolint:lll chanArb.cfg.ChainEvents.LocalUnilateralClosure <- &LocalUnilateralCloseInfo{ - SpendDetail: &chainntnfs.SpendDetail{}, + SpendDetail: &chainntnfs.SpendDetail{ + SpendingHeight: int32(spendingHeight), + }, LocalForceCloseSummary: &lnwallet.LocalForceCloseSummary{ CloseTx: closeTx, ContractResolutions: fn.Some(lnwallet.ContractResolutions{ @@ -2989,16 +2992,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], ) } @@ -3146,7 +3147,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 c608882d4e..c66c5aeabf 100644 --- a/contractcourt/contract_resolver.go +++ b/contractcourt/contract_resolver.go @@ -38,6 +38,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 b92fcb678f..92ebecd836 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 29521a1bec30ec55e7f5d9d04373a1c1659c728d Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Mon, 25 Nov 2024 13:55:11 +0800 Subject: [PATCH 050/153] 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 fe8419a59e..157cf87af0 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 097079e51e97197fdf68e2809340ae87fb21c9ab Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 20 Jun 2024 23:01:36 +0800 Subject: [PATCH 051/153] 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 808f41f2ee..b7475f03c8 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" @@ -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 8f6f9e2bfb30c69fb118612405b11a4f4176a0ab Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 16 Nov 2024 08:35:28 +0800 Subject: [PATCH 052/153] 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 b7475f03c8..07bb95b8f6 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 program 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 6870b736d112daf920f157a1500c82806292424a Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 16 Nov 2024 08:42:04 +0800 Subject: [PATCH 053/153] 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 07bb95b8f6..01abd667cf 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 9631dbbb483421b11bba5ab3e38503aa96813266 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 16 Nov 2024 08:56:28 +0800 Subject: [PATCH 054/153] contractcourt: handle blockbeat in `chainWatcher` --- contractcourt/chain_watcher.go | 163 +++++++++++++++++++++------- contractcourt/chain_watcher_test.go | 125 +++++++++++++++++++-- 2 files changed, 236 insertions(+), 52 deletions(-) diff --git a/contractcourt/chain_watcher.go b/contractcourt/chain_watcher.go index 01abd667cf..90a2cecb1d 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,37 @@ 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. + 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 +1433,101 @@ 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 { + 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 baea8d8738..0b3e23e714 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 ca9216025bbf2c0aef8d2a541616107589c1f4b1 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 16 Nov 2024 08:56:48 +0800 Subject: [PATCH 055/153] 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 71a4545707..632cc256f2 100644 --- a/contractcourt/chain_arbitrator.go +++ b/contractcourt/chain_arbitrator.go @@ -731,19 +731,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 530ac1b5f12eb8f6164b693d1b641211a800ec7f Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 28 Jun 2024 12:07:15 +0800 Subject: [PATCH 056/153] contractcourt: provide a shortcut to `ChannelPoint` This way the log lines are shorten. --- contractcourt/chain_arbitrator.go | 11 +- contractcourt/channel_arbitrator.go | 207 ++++++++++++++-------------- contractcourt/contract_resolver.go | 2 +- 3 files changed, 111 insertions(+), 109 deletions(-) diff --git a/contractcourt/chain_arbitrator.go b/contractcourt/chain_arbitrator.go index 632cc256f2..5dc95ade19 100644 --- a/contractcourt/chain_arbitrator.go +++ b/contractcourt/chain_arbitrator.go @@ -549,7 +549,7 @@ func (c *ChainArbitrator) ResolveContract(chanPoint wire.OutPoint) error { if err := chainArb.Stop(); err != nil { log.Warnf("unable to stop ChannelArbitrator(%v): %v", - chanPoint, err) + chainArb.id(), err) } } if chainWatcher != nil { @@ -891,7 +891,7 @@ func (c *ChainArbitrator) Stop() error { } for chanPoint, arbitrator := range activeChannels { log.Tracef("Attempting to stop ChannelArbitrator(%v)", - chanPoint) + arbitrator.id()) if err := arbitrator.Stop(); err != nil { log.Errorf("unable to stop arbitrator for "+ @@ -1066,7 +1066,7 @@ func (c *ChainArbitrator) WatchNewChannel(newChan *channeldb.OpenChannel) error chanPoint := newChan.FundingOutpoint log.Infof("Creating new ChannelArbitrator for ChannelPoint(%v)", - chanPoint) + newChan.FundingOutpoint) // If we're already watching this channel, then we'll ignore this // request. @@ -1208,8 +1208,9 @@ func (c *ChainArbitrator) FindOutgoingHTLCDeadline(scid lnwire.ShortChannelID, log.Debugf("ChannelArbitrator(%v): found "+ "incoming HTLC in channel=%v using "+ - "rHash=%x, refundTimeout=%v", scid, - cp, rHash, htlc.RefundTimeout) + "rHash=%x, refundTimeout=%v", + channelArb.id(), cp, rHash, + htlc.RefundTimeout) return fn.Some(int32(htlc.RefundTimeout)) } diff --git a/contractcourt/channel_arbitrator.go b/contractcourt/channel_arbitrator.go index 2b3551e75f..9400ea9291 100644 --- a/contractcourt/channel_arbitrator.go +++ b/contractcourt/channel_arbitrator.go @@ -480,7 +480,7 @@ func (c *ChannelArbitrator) Start(state *chanArbStartState, } log.Debugf("Starting ChannelArbitrator(%v), htlc_set=%v, state=%v", - c.cfg.ChanPoint, lnutils.SpewLogClosure(c.activeHTLCs), + c.id(), lnutils.SpewLogClosure(c.activeHTLCs), state.currentState) // Set our state from our starting state. @@ -519,16 +519,14 @@ func (c *ChannelArbitrator) Start(state *chanArbStartState, } log.Warnf("ChannelArbitrator(%v): detected stalled "+ - "state=%v for closed channel", - c.cfg.ChanPoint, c.state) + "state=%v for closed channel", c.id(), c.state) } triggerHeight = c.cfg.ClosingHeight } log.Infof("ChannelArbitrator(%v): starting state=%v, trigger=%v, "+ - "triggerHeight=%v", c.cfg.ChanPoint, c.state, trigger, - triggerHeight) + "triggerHeight=%v", c.id(), c.state, trigger, triggerHeight) // We'll now attempt to advance our state forward based on the current // on-chain state, and our set of active contracts. @@ -547,8 +545,8 @@ func (c *ChannelArbitrator) Start(state *chanArbStartState, fallthrough case errNoResolutions: log.Warnf("ChannelArbitrator(%v): detected closed"+ - "channel with no contract resolutions written.", - c.cfg.ChanPoint) + "channel with no contract resolutions written", + c.id()) default: return err @@ -737,14 +735,14 @@ func (c *ChannelArbitrator) relaunchResolvers(commitSet *CommitSet, fallthrough case err == channeldb.ErrChannelNotFound: log.Warnf("ChannelArbitrator(%v): unable to fetch historical "+ - "state", c.cfg.ChanPoint) + "state", c.id()) case err != nil: return err } log.Infof("ChannelArbitrator(%v): relaunching %v contract "+ - "resolvers", c.cfg.ChanPoint, len(unresolvedContracts)) + "resolvers", c.id(), len(unresolvedContracts)) for i := range unresolvedContracts { resolver := unresolvedContracts[i] @@ -838,7 +836,7 @@ func (c *ChannelArbitrator) Stop() error { return nil } - log.Debugf("Stopping ChannelArbitrator(%v)", c.cfg.ChanPoint) + log.Debugf("Stopping ChannelArbitrator(%v)", c.id()) if c.cfg.ChainEvents.Cancel != nil { go c.cfg.ChainEvents.Cancel() @@ -936,8 +934,7 @@ func (c *ChannelArbitrator) stateStep( // to see if while we were down, conditions have changed. case StateDefault: log.Debugf("ChannelArbitrator(%v): new block (height=%v) "+ - "examining active HTLC's", c.cfg.ChanPoint, - triggerHeight) + "examining active HTLC's", c.id(), triggerHeight) // As a new block has been connected to the end of the main // chain, we'll check to see if we need to make any on-chain @@ -982,7 +979,7 @@ func (c *ChannelArbitrator) stateStep( // checking due to a chain update), then we'll exit now. if len(chainActions) == 0 && trigger == chainTrigger { log.Debugf("ChannelArbitrator(%v): no actions for "+ - "chain trigger, terminating", c.cfg.ChanPoint) + "chain trigger, terminating", c.id()) return StateDefault, closeTx, nil } @@ -990,7 +987,7 @@ func (c *ChannelArbitrator) stateStep( // Otherwise, we'll log that we checked the HTLC actions as the // commitment transaction has already been broadcast. log.Tracef("ChannelArbitrator(%v): logging chain_actions=%v", - c.cfg.ChanPoint, lnutils.SpewLogClosure(chainActions)) + c.id(), lnutils.SpewLogClosure(chainActions)) // Cancel upstream HTLCs for all outgoing dust HTLCs available // either on the local or the remote/remote pending commitment @@ -1037,7 +1034,7 @@ func (c *ChannelArbitrator) stateStep( case localCloseTrigger: log.Errorf("ChannelArbitrator(%v): unexpected local "+ "commitment confirmed while in StateDefault", - c.cfg.ChanPoint) + c.id()) fallthrough case remoteCloseTrigger: nextState = StateContractClosed @@ -1068,7 +1065,8 @@ func (c *ChannelArbitrator) stateStep( log.Infof("ChannelArbitrator(%v): detected %s "+ "close after closing channel, fast-forwarding "+ "to %s to resolve contract", - c.cfg.ChanPoint, trigger, StateContractClosed) + c.id(), trigger, StateContractClosed) + return StateContractClosed, closeTx, nil case breachCloseTrigger: @@ -1076,13 +1074,13 @@ func (c *ChannelArbitrator) stateStep( if nextContractState == StateError { log.Infof("ChannelArbitrator(%v): unable to "+ "advance breach close resolution: %v", - c.cfg.ChanPoint, nextContractState) + c.id(), nextContractState) return StateError, closeTx, err } log.Infof("ChannelArbitrator(%v): detected %s close "+ "after closing channel, fast-forwarding to %s"+ - " to resolve contract", c.cfg.ChanPoint, + " to resolve contract", c.id(), trigger, nextContractState) return nextContractState, closeTx, nil @@ -1091,12 +1089,13 @@ func (c *ChannelArbitrator) stateStep( log.Infof("ChannelArbitrator(%v): detected %s "+ "close after closing channel, fast-forwarding "+ "to %s to resolve contract", - c.cfg.ChanPoint, trigger, StateFullyResolved) + c.id(), trigger, StateFullyResolved) + return StateFullyResolved, closeTx, nil } log.Infof("ChannelArbitrator(%v): force closing "+ - "chan", c.cfg.ChanPoint) + "chan", c.id()) // Now that we have all the actions decided for the set of // HTLC's, we'll broadcast the commitment transaction, and @@ -1108,7 +1107,7 @@ func (c *ChannelArbitrator) stateStep( forceCloseTx, err := c.cfg.Channel.ForceCloseChan() if err != nil { log.Errorf("ChannelArbitrator(%v): unable to "+ - "force close: %v", c.cfg.ChanPoint, err) + "force close: %v", c.id(), err) // We tried to force close (HTLC may be expiring from // our PoV, etc), but we think we've lost data. In this @@ -1118,7 +1117,7 @@ func (c *ChannelArbitrator) stateStep( log.Error("ChannelArbitrator(%v): broadcast "+ "failed due to local data loss, "+ "waiting for on chain confimation...", - c.cfg.ChanPoint) + c.id()) return StateBroadcastCommit, nil, nil } @@ -1134,8 +1133,8 @@ func (c *ChannelArbitrator) stateStep( err = c.cfg.MarkCommitmentBroadcasted(closeTx, lntypes.Local) if err != nil { log.Errorf("ChannelArbitrator(%v): unable to "+ - "mark commitment broadcasted: %v", - c.cfg.ChanPoint, err) + "mark commitment broadcasted: %v", c.id(), err) + return StateError, closeTx, err } @@ -1153,7 +1152,7 @@ func (c *ChannelArbitrator) stateStep( ) if err := c.cfg.PublishTx(closeTx, label); err != nil { log.Errorf("ChannelArbitrator(%v): unable to broadcast "+ - "close tx: %v", c.cfg.ChanPoint, err) + "close tx: %v", c.id(), err) // This makes sure we don't fail at startup if the // commitment transaction has too low fees to make it @@ -1224,7 +1223,7 @@ func (c *ChannelArbitrator) stateStep( } log.Infof("ChannelArbitrator(%v): trigger %v moving from "+ - "state %v to %v", c.cfg.ChanPoint, trigger, c.state, + "state %v to %v", c.id(), trigger, c.state, nextState) // If we're in this state, then the contract has been fully closed to @@ -1245,8 +1244,8 @@ func (c *ChannelArbitrator) stateStep( // resolvers, and can go straight to our final state. if contractResolutions.IsEmpty() && confCommitSet.IsEmpty() { log.Infof("ChannelArbitrator(%v): contract "+ - "resolutions empty, marking channel as fully resolved!", - c.cfg.ChanPoint) + "resolutions empty, marking channel as fully "+ + "resolved!", c.id()) nextState = StateFullyResolved break } @@ -1329,12 +1328,13 @@ func (c *ChannelArbitrator) stateStep( ) if err != nil { log.Errorf("ChannelArbitrator(%v): unable to "+ - "resolve contracts: %v", c.cfg.ChanPoint, err) + "resolve contracts: %v", c.id(), err) + return StateError, closeTx, err } log.Debugf("ChannelArbitrator(%v): inserting %v contract "+ - "resolvers", c.cfg.ChanPoint, len(resolvers)) + "resolvers", c.id(), len(resolvers)) err = c.log.InsertUnresolvedContracts(nil, resolvers...) if err != nil { @@ -1351,7 +1351,7 @@ func (c *ChannelArbitrator) stateStep( // all contracts are fully resolved. case StateWaitingFullResolution: log.Infof("ChannelArbitrator(%v): still awaiting contract "+ - "resolution", c.cfg.ChanPoint) + "resolution", c.id()) unresolved, err := c.log.FetchUnresolvedContracts() if err != nil { @@ -1372,7 +1372,7 @@ func (c *ChannelArbitrator) stateStep( // Add debug logs. for _, r := range unresolved { log.Debugf("ChannelArbitrator(%v): still have "+ - "unresolved contract: %T", c.cfg.ChanPoint, r) + "unresolved contract: %T", c.id(), r) } // If we start as fully resolved, then we'll end as fully resolved. @@ -1391,8 +1391,7 @@ func (c *ChannelArbitrator) stateStep( } } - log.Tracef("ChannelArbitrator(%v): next_state=%v", c.cfg.ChanPoint, - nextState) + log.Tracef("ChannelArbitrator(%v): next_state=%v", c.id(), nextState) return nextState, closeTx, nil } @@ -1456,8 +1455,7 @@ func (c *ChannelArbitrator) findCommitmentDeadlineAndValue(heightHint uint32, // Skip if the HTLC is dust. if htlc.OutputIndex < 0 { log.Debugf("ChannelArbitrator(%v): skipped deadline "+ - "for dust htlc=%x", - c.cfg.ChanPoint, htlc.RHash[:]) + "for dust htlc=%x", c.id(), htlc.RHash[:]) continue } @@ -1483,7 +1481,7 @@ func (c *ChannelArbitrator) findCommitmentDeadlineAndValue(heightHint uint32, deadlineMinHeight = deadline log.Tracef("ChannelArbitrator(%v): outgoing HTLC has "+ - "deadline=%v, value=%v", c.cfg.ChanPoint, + "deadline=%v, value=%v", c.id(), deadlineMinHeight, value) } } @@ -1494,8 +1492,7 @@ func (c *ChannelArbitrator) findCommitmentDeadlineAndValue(heightHint uint32, // Skip if the HTLC is dust. if htlc.OutputIndex < 0 { log.Debugf("ChannelArbitrator(%v): skipped deadline "+ - "for dust htlc=%x", - c.cfg.ChanPoint, htlc.RHash[:]) + "for dust htlc=%x", c.id(), htlc.RHash[:]) continue } @@ -1518,7 +1515,7 @@ func (c *ChannelArbitrator) findCommitmentDeadlineAndValue(heightHint uint32, deadlineMinHeight = htlc.RefundTimeout log.Tracef("ChannelArbitrator(%v): incoming HTLC has "+ - "deadline=%v, amt=%v", c.cfg.ChanPoint, + "deadline=%v, amt=%v", c.id(), deadlineMinHeight, value) } } @@ -1542,7 +1539,7 @@ func (c *ChannelArbitrator) findCommitmentDeadlineAndValue(heightHint uint32, case deadlineMinHeight <= heightHint: log.Warnf("ChannelArbitrator(%v): deadline is passed with "+ "deadlineMinHeight=%d, heightHint=%d", - c.cfg.ChanPoint, deadlineMinHeight, heightHint) + c.id(), deadlineMinHeight, heightHint) deadline = 1 // Use half of the deadline delta, and leave the other half to be used @@ -1560,8 +1557,7 @@ func (c *ChannelArbitrator) findCommitmentDeadlineAndValue(heightHint uint32, log.Debugf("ChannelArbitrator(%v): calculated valueLeft=%v, "+ "deadline=%d, using deadlineMinHeight=%d, heightHint=%d", - c.cfg.ChanPoint, valueLeft, deadline, deadlineMinHeight, - heightHint) + c.id(), valueLeft, deadline, deadlineMinHeight, heightHint) return fn.Some(int32(deadline)), valueLeft, nil } @@ -1598,8 +1594,7 @@ func (c *ChannelArbitrator) launchResolvers() { // launch it again. if r.IsResolved() { log.Debugf("ChannelArbitrator(%v): skipping resolver "+ - "%T as it's already resolved", c.cfg.ChanPoint, - r) + "%T as it's already resolved", c.id(), r) continue } @@ -1623,8 +1618,7 @@ func (c *ChannelArbitrator) launchResolvers() { } log.Errorf("ChannelArbitrator(%v): unable to launch "+ - "contract resolver(%T): %v", c.cfg.ChanPoint, r, - err) + "contract resolver(%T): %v", c.id(), r, err) case <-c.quit: log.Debugf("ChannelArbitrator quit signal received, " + @@ -1656,14 +1650,15 @@ func (c *ChannelArbitrator) advanceState( priorState = c.state log.Debugf("ChannelArbitrator(%v): attempting state step with "+ "trigger=%v from state=%v at height=%v", - c.cfg.ChanPoint, trigger, priorState, triggerHeight) + c.id(), trigger, priorState, triggerHeight) nextState, closeTx, err := c.stateStep( triggerHeight, trigger, confCommitSet, ) if err != nil { log.Errorf("ChannelArbitrator(%v): unable to advance "+ - "state: %v", c.cfg.ChanPoint, err) + "state: %v", c.id(), err) + return priorState, nil, err } @@ -1676,7 +1671,8 @@ func (c *ChannelArbitrator) advanceState( // terminate. if nextState == priorState { log.Debugf("ChannelArbitrator(%v): terminating at "+ - "state=%v", c.cfg.ChanPoint, nextState) + "state=%v", c.id(), nextState) + return nextState, forceCloseTx, nil } @@ -1685,8 +1681,8 @@ func (c *ChannelArbitrator) advanceState( // the prior state if anything fails. if err := c.log.CommitState(nextState); err != nil { log.Errorf("ChannelArbitrator(%v): unable to commit "+ - "next state(%v): %v", c.cfg.ChanPoint, - nextState, err) + "next state(%v): %v", c.id(), nextState, err) + return priorState, nil, err } c.state = nextState @@ -1800,8 +1796,8 @@ func (c *ChannelArbitrator) shouldGoOnChain(htlc channeldb.HTLC, broadcastCutOff := htlc.RefundTimeout - broadcastDelta log.Tracef("ChannelArbitrator(%v): examining outgoing contract: "+ - "expiry=%v, cutoff=%v, height=%v", c.cfg.ChanPoint, htlc.RefundTimeout, - broadcastCutOff, currentHeight) + "expiry=%v, cutoff=%v, height=%v", c.id(), + htlc.RefundTimeout, broadcastCutOff, currentHeight) // TODO(roasbeef): take into account default HTLC delta, don't need to // broadcast immediately @@ -1850,8 +1846,8 @@ func (c *ChannelArbitrator) checkCommitChainActions(height uint32, log.Debugf("ChannelArbitrator(%v): checking commit chain actions at "+ "height=%v, in_htlc_count=%v, out_htlc_count=%v", - c.cfg.ChanPoint, height, - len(htlcs.incomingHTLCs), len(htlcs.outgoingHTLCs)) + c.id(), height, len(htlcs.incomingHTLCs), + len(htlcs.outgoingHTLCs)) actionMap := make(ChainActionMap) @@ -1880,7 +1876,7 @@ func (c *ChannelArbitrator) checkCommitChainActions(height uint32, log.Infof("ChannelArbitrator(%v): go to chain for "+ "outgoing htlc %x: timeout=%v, amount=%v, "+ "blocks_until_expiry=%v, broadcast_delta=%v", - c.cfg.ChanPoint, htlc.RHash[:], + c.id(), htlc.RHash[:], htlc.RefundTimeout, htlc.Amt, remainingBlocks, c.cfg.OutgoingBroadcastDelta, ) @@ -1915,7 +1911,7 @@ func (c *ChannelArbitrator) checkCommitChainActions(height uint32, log.Infof("ChannelArbitrator(%v): go to chain for "+ "incoming htlc %x: timeout=%v, amount=%v, "+ "blocks_until_expiry=%v, broadcast_delta=%v", - c.cfg.ChanPoint, htlc.RHash[:], + c.id(), htlc.RHash[:], htlc.RefundTimeout, htlc.Amt, remainingBlocks, c.cfg.IncomingBroadcastDelta, ) @@ -1930,7 +1926,7 @@ func (c *ChannelArbitrator) checkCommitChainActions(height uint32, // we're *forced* to act on each HTLC. if !haveChainActions && trigger == chainTrigger { log.Tracef("ChannelArbitrator(%v): no actions to take at "+ - "height=%v", c.cfg.ChanPoint, height) + "height=%v", c.id(), height) return actionMap, nil } @@ -1946,8 +1942,7 @@ func (c *ChannelArbitrator) checkCommitChainActions(height uint32, // negative. case htlc.OutputIndex < 0: log.Tracef("ChannelArbitrator(%v): immediately "+ - "failing dust htlc=%x", c.cfg.ChanPoint, - htlc.RHash[:]) + "failing dust htlc=%x", c.id(), htlc.RHash[:]) actionMap[HtlcFailDustAction] = append( actionMap[HtlcFailDustAction], htlc, @@ -1969,7 +1964,7 @@ func (c *ChannelArbitrator) checkCommitChainActions(height uint32, log.Tracef("ChannelArbitrator(%v): watching chain to "+ "decide action for outgoing htlc=%x", - c.cfg.ChanPoint, htlc.RHash[:]) + c.id(), htlc.RHash[:]) actionMap[HtlcOutgoingWatchAction] = append( actionMap[HtlcOutgoingWatchAction], htlc, @@ -1979,7 +1974,7 @@ func (c *ChannelArbitrator) checkCommitChainActions(height uint32, // to sweep this HTLC on-chain default: log.Tracef("ChannelArbitrator(%v): going on-chain to "+ - "timeout htlc=%x", c.cfg.ChanPoint, htlc.RHash[:]) + "timeout htlc=%x", c.id(), htlc.RHash[:]) actionMap[HtlcTimeoutAction] = append( actionMap[HtlcTimeoutAction], htlc, @@ -1997,7 +1992,7 @@ func (c *ChannelArbitrator) checkCommitChainActions(height uint32, if htlc.OutputIndex < 0 { log.Debugf("ChannelArbitrator(%v): no resolution "+ "needed for incoming dust htlc=%x", - c.cfg.ChanPoint, htlc.RHash[:]) + c.id(), htlc.RHash[:]) actionMap[HtlcIncomingDustFinalAction] = append( actionMap[HtlcIncomingDustFinalAction], htlc, @@ -2007,8 +2002,7 @@ func (c *ChannelArbitrator) checkCommitChainActions(height uint32, } log.Tracef("ChannelArbitrator(%v): watching chain to decide "+ - "action for incoming htlc=%x", c.cfg.ChanPoint, - htlc.RHash[:]) + "action for incoming htlc=%x", c.id(), htlc.RHash[:]) actionMap[HtlcIncomingWatchAction] = append( actionMap[HtlcIncomingWatchAction], htlc, @@ -2162,8 +2156,7 @@ func (c *ChannelArbitrator) checkRemoteDanglingActions( } log.Infof("ChannelArbitrator(%v): fail dangling htlc=%x from "+ - "local/remote commitments diff", - c.cfg.ChanPoint, htlc.RHash[:]) + "local/remote commitments diff", c.id(), htlc.RHash[:]) actionMap[HtlcFailDanglingAction] = append( actionMap[HtlcFailDanglingAction], htlc, @@ -2245,8 +2238,7 @@ func (c *ChannelArbitrator) checkRemoteDiffActions( if err != nil { log.Errorf("ChannelArbitrator(%v): failed to query "+ "preimage for dangling htlc=%x from remote "+ - "commitments diff", c.cfg.ChanPoint, - htlc.RHash[:]) + "commitments diff", c.id(), htlc.RHash[:]) continue } @@ -2273,8 +2265,7 @@ func (c *ChannelArbitrator) checkRemoteDiffActions( ) log.Infof("ChannelArbitrator(%v): fail dangling htlc=%x from "+ - "remote commitments diff", - c.cfg.ChanPoint, htlc.RHash[:]) + "remote commitments diff", c.id(), htlc.RHash[:]) } return actionMap @@ -2355,7 +2346,7 @@ func (c *ChannelArbitrator) prepContractResolutions( fallthrough case err == channeldb.ErrChannelNotFound: log.Warnf("ChannelArbitrator(%v): unable to fetch historical "+ - "state", c.cfg.ChanPoint) + "state", c.id()) case err != nil: return nil, err @@ -2436,7 +2427,7 @@ func (c *ChannelArbitrator) prepContractResolutions( // TODO(roasbeef): panic? log.Errorf("ChannelArbitrator(%v) unable to find "+ "incoming resolution: %v", - c.cfg.ChanPoint, htlcOp) + c.id(), htlcOp) continue } @@ -2463,8 +2454,10 @@ func (c *ChannelArbitrator) prepContractResolutions( resolution, ok := outResolutionMap[htlcOp] if !ok { - log.Errorf("ChannelArbitrator(%v) unable to find "+ - "outgoing resolution: %v", c.cfg.ChanPoint, htlcOp) + log.Errorf("ChannelArbitrator(%v) "+ + "unable to find outgoing "+ + "resolution: %v", + c.id(), htlcOp) continue } @@ -2504,7 +2497,7 @@ func (c *ChannelArbitrator) prepContractResolutions( if !ok { log.Errorf("ChannelArbitrator(%v) unable to find "+ "incoming resolution: %v", - c.cfg.ChanPoint, htlcOp) + c.id(), htlcOp) continue } @@ -2535,7 +2528,7 @@ func (c *ChannelArbitrator) prepContractResolutions( log.Errorf("ChannelArbitrator(%v) "+ "unable to find outgoing "+ "resolution: %v", - c.cfg.ChanPoint, htlcOp) + c.id(), htlcOp) continue } @@ -2607,13 +2600,13 @@ func (c *ChannelArbitrator) resolveContract(currentContract ContractResolver) { defer c.wg.Done() log.Debugf("ChannelArbitrator(%v): attempting to resolve %T", - c.cfg.ChanPoint, currentContract) + c.id(), 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.Debugf("ChannelArbitrator(%v): contract %T not yet "+ + "resolved", c.id(), currentContract) select { @@ -2632,7 +2625,7 @@ func (c *ChannelArbitrator) resolveContract(currentContract ContractResolver) { log.Errorf("ChannelArbitrator(%v): unable to "+ "progress %T: %v", - c.cfg.ChanPoint, currentContract, err) + c.id(), currentContract, err) return } @@ -2645,7 +2638,7 @@ func (c *ChannelArbitrator) resolveContract(currentContract ContractResolver) { case nextContract != nil: log.Debugf("ChannelArbitrator(%v): swapping "+ "out contract %T for %T ", - c.cfg.ChanPoint, currentContract, + c.id(), currentContract, nextContract) // Swap contract in log. @@ -2685,7 +2678,7 @@ func (c *ChannelArbitrator) resolveContract(currentContract ContractResolver) { case currentContract.IsResolved(): log.Debugf("ChannelArbitrator(%v): marking "+ "contract %T fully resolved", - c.cfg.ChanPoint, currentContract) + c.id(), currentContract) err := c.log.ResolveContract(currentContract) if err != nil { @@ -2751,7 +2744,7 @@ func (c *ChannelArbitrator) notifyContractUpdate(upd *ContractUpdate) { c.unmergedSet[upd.HtlcKey] = newHtlcSet(upd.Htlcs) log.Tracef("ChannelArbitrator(%v): fresh set of htlcs=%v", - c.cfg.ChanPoint, lnutils.SpewLogClosure(upd)) + c.id(), lnutils.SpewLogClosure(upd)) } // updateActiveHTLCs merges the unmerged set of HTLCs from the link with @@ -2800,7 +2793,7 @@ func (c *ChannelArbitrator) channelAttendant(bestHeight int32) { bestHeight = beat.Height() log.Debugf("ChannelArbitrator(%v): new block height=%v", - c.cfg.ChanPoint, bestHeight) + c.id(), bestHeight) err := c.handleBlockbeat(beat) if err != nil { @@ -2820,7 +2813,7 @@ func (c *ChannelArbitrator) channelAttendant(bestHeight int32) { // properly do our job. case signalUpdate := <-c.signalUpdates: log.Tracef("ChannelArbitrator(%v): got new signal "+ - "update!", c.cfg.ChanPoint) + "update!", c.id()) // We'll update the ShortChannelID. c.cfg.ShortChanID = signalUpdate.newSignals.ShortChanID @@ -2834,7 +2827,9 @@ 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.id(), + closeInfo.CloseHeight, + ) err := c.cfg.MarkChannelClosed( closeInfo.ChannelCloseSummary, @@ -2861,8 +2856,7 @@ func (c *ChannelArbitrator) channelAttendant(bestHeight int32) { case closeInfo := <-c.cfg.ChainEvents.LocalUnilateralClosure: if c.state != StateCommitmentBroadcasted { log.Errorf("ChannelArbitrator(%v): unexpected "+ - "local on-chain channel close", - c.cfg.ChanPoint) + "local on-chain channel close", c.id()) } closeTx := closeInfo.CloseTx @@ -2893,8 +2887,7 @@ func (c *ChannelArbitrator) channelAttendant(bestHeight int32) { } log.Infof("ChannelArbitrator(%v): local force close "+ - "tx=%v confirmed", c.cfg.ChanPoint, - closeTx.TxHash()) + "tx=%v confirmed", c.id(), closeTx.TxHash()) contractRes := &ContractResolutions{ CommitHash: closeTx.TxHash(), @@ -2959,7 +2952,9 @@ 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.id(), + uniClosure.SpendingHeight, + ) // If we don't have a self output, and there are no // active HTLC's, then we can immediately mark the @@ -3028,8 +3023,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.id(), + closeSummary.CloseHeight) // In the breach case, we'll only have anchor and // breach resolutions. @@ -3061,7 +3059,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, @@ -3078,8 +3075,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) @@ -3090,7 +3087,7 @@ func (c *ChannelArbitrator) channelAttendant(bestHeight int32) { // we can exit as the contract is fully resolved. case <-c.resolutionSignal: log.Infof("ChannelArbitrator(%v): a contract has been "+ - "fully resolved!", c.cfg.ChanPoint) + "fully resolved!", c.id()) nextState, _, err := c.advanceState( uint32(bestHeight), chainTrigger, nil, @@ -3104,7 +3101,7 @@ func (c *ChannelArbitrator) channelAttendant(bestHeight int32) { if nextState == StateFullyResolved { log.Infof("ChannelArbitrator(%v): all "+ "contracts fully resolved, exiting", - c.cfg.ChanPoint) + c.id()) return } @@ -3113,7 +3110,7 @@ func (c *ChannelArbitrator) channelAttendant(bestHeight int32) { // channel. We'll case closeReq := <-c.forceCloseReqs: log.Infof("ChannelArbitrator(%v): received force "+ - "close request", c.cfg.ChanPoint) + "close request", c.id()) if c.state != StateDefault { select { @@ -3152,8 +3149,7 @@ func (c *ChannelArbitrator) channelAttendant(bestHeight int32) { // advancing our state, then we'll exit. if nextState == StateFullyResolved { log.Infof("ChannelArbitrator(%v): all "+ - "contracts resolved, exiting", - c.cfg.ChanPoint) + "contracts resolved, exiting", c.id()) return } @@ -3191,7 +3187,7 @@ func (c *ChannelArbitrator) handleBlockbeat(beat chainio.Blockbeat) error { // // NOTE: Part of chainio.Consumer interface. func (c *ChannelArbitrator) Name() string { - return fmt.Sprintf("ChannelArbitrator(%v)", c.cfg.ChanPoint) + return fmt.Sprintf("ChannelArbitrator(%v)", c.id()) } // checkLegacyBreach returns StateFullyResolved if the channel was closed with @@ -3479,3 +3475,8 @@ func (c *ChannelArbitrator) abandonForwards(htlcs fn.Set[uint64]) error { return nil } + +// id is a shortcut to the channel arbitrator's chan point. +func (c *ChannelArbitrator) id() string { + return c.cfg.ChanPoint.String() +} diff --git a/contractcourt/contract_resolver.go b/contractcourt/contract_resolver.go index c66c5aeabf..9219313699 100644 --- a/contractcourt/contract_resolver.go +++ b/contractcourt/contract_resolver.go @@ -150,7 +150,7 @@ func newContractResolverKit(cfg ResolverConfig) *contractResolverKit { // initLogger initializes the resolver-specific logger. func (r *contractResolverKit) initLogger(prefix string) { - logPrefix := fmt.Sprintf("ChannelArbitrator(%v): %s:", r.ChanPoint, + logPrefix := fmt.Sprintf("ChannelArbitrator(%v): %s:", r.ShortChanID, prefix) r.log = build.NewPrefixLog(logPrefix, log) From 808e3a6735664637e3fd65e87ac26fe64db08889 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 14 Nov 2024 02:04:00 +0800 Subject: [PATCH 057/153] 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 | 39 +++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/chanrestore.go b/chanrestore.go index 5b221c105a..a041f571a8 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 5dc95ade19..fef8f00a20 100644 --- a/contractcourt/chain_arbitrator.go +++ b/contractcourt/chain_arbitrator.go @@ -1376,3 +1376,42 @@ 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 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 failed: %v", err) + } +} From 651d4d476e6b6bd9a3332bc0eaf99ed9b59df149 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 16 Nov 2024 09:02:00 +0800 Subject: [PATCH 058/153] 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 | 453 ++++++++++++++-------------- 1 file changed, 234 insertions(+), 219 deletions(-) diff --git a/contractcourt/channel_arbitrator.go b/contractcourt/channel_arbitrator.go index 9400ea9291..740c5d0e12 100644 --- a/contractcourt/channel_arbitrator.go +++ b/contractcourt/channel_arbitrator.go @@ -2826,28 +2826,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.id(), - 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 } @@ -2859,229 +2842,39 @@ func (c *ChannelArbitrator) channelAttendant(bestHeight int32) { "local on-chain channel close", c.id()) } - closeTx := closeInfo.CloseTx - - resolutions, err := closeInfo.ContractResolutions. - UnwrapOrErr( - fmt.Errorf("resolutions not found"), - ) - 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) - - return - } - - log.Infof("ChannelArbitrator(%v): local force close "+ - "tx=%v confirmed", c.id(), 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, - ) + err := c.handleLocalForceCloseEvent(closeInfo) if err != nil { - log.Errorf("Unable to write commit set: %v", - err) - return - } + log.Errorf("Failed to handle local 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. - 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.id(), - 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.id(), - 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. @@ -3480,3 +3273,225 @@ func (c *ChannelArbitrator) abandonForwards(htlcs fn.Set[uint64]) error { func (c *ChannelArbitrator) id() string { return c.cfg.ChanPoint.String() } + +// 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.id(), 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.id(), 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.id(), 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.id(), 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 b058383f0ab66a0aa2baa2c8c81f968b6aa85ced Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 14 Nov 2024 01:39:36 +0800 Subject: [PATCH 059/153] contractcourt: process channel close event on new beat --- contractcourt/chain_arbitrator.go | 12 +++--- contractcourt/channel_arbitrator.go | 57 +++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/contractcourt/chain_arbitrator.go b/contractcourt/chain_arbitrator.go index fef8f00a20..744bf4b4bc 100644 --- a/contractcourt/chain_arbitrator.go +++ b/contractcourt/chain_arbitrator.go @@ -582,9 +582,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 { @@ -694,6 +691,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 @@ -1065,8 +1067,8 @@ func (c *ChainArbitrator) WatchNewChannel(newChan *channeldb.OpenChannel) error chanPoint := newChan.FundingOutpoint - log.Infof("Creating new ChannelArbitrator for ChannelPoint(%v)", - newChan.FundingOutpoint) + log.Infof("Creating new Chainwatcher and ChannelArbitrator for "+ + "ChannelPoint(%v)", newChan.FundingOutpoint) // 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 740c5d0e12..00edf22f09 100644 --- a/contractcourt/channel_arbitrator.go +++ b/contractcourt/channel_arbitrator.go @@ -2958,6 +2958,10 @@ func (c *ChannelArbitrator) handleBlockbeat(beat chainio.Blockbeat) error { // Notify we've processed the block. defer c.NotifyBlockProcessed(beat, 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 @@ -2976,6 +2980,59 @@ 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.id()) + } + + 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.id()) + } +} + // Name returns a human-readable string for this subsystem. // // NOTE: Part of chainio.Consumer interface. From 3d48f0ca97b85f0d125a3274a48b00074ef40e3d Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 22 Nov 2024 22:28:51 +0800 Subject: [PATCH 060/153] contractcourt: register conf notification once and cancel when confirmed --- contractcourt/chain_watcher.go | 40 ++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/contractcourt/chain_watcher.go b/contractcourt/chain_watcher.go index 90a2cecb1d..4edddcd932 100644 --- a/contractcourt/chain_watcher.go +++ b/contractcourt/chain_watcher.go @@ -245,6 +245,11 @@ 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. + fundingConfirmedNtfn *chainntnfs.ConfirmationEvent } // newChainWatcher returns a new instance of a chainWatcher for a channel given @@ -297,6 +302,20 @@ func newChainWatcher(cfg chainWatcherConfig) (*chainWatcher, error) { fundingSpendNtfn: spendNtfn, } + // If this is a pending taproot channel, we need to register for a + // confirmation notification of the funding tx. + 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()) @@ -1469,17 +1488,9 @@ func (c *chainWatcher) checkFundingSpend() *chainntnfs.SpendDetail { // 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: + case _, ok := <-c.fundingConfirmedNtfn.Confirmed: // If the channel was closed, then this means that the notifier // exited, so we will as well. if !ok { @@ -1488,6 +1499,10 @@ func (c *chainWatcher) chanPointConfirmed() bool { 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: @@ -1504,9 +1519,10 @@ 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. + 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 d7854694b38dc939abb6da91dc8c513426be2e89 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Mon, 25 Nov 2024 14:04:18 +0800 Subject: [PATCH 061/153] 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 71b9c929df..12366abf95 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 5c9c6d09756dbcb06f0944add5671665ec86dda8 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Mon, 25 Nov 2024 14:19:23 +0800 Subject: [PATCH 062/153] 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 6fa3c0f063..e9d64fc677 100644 --- a/docs/release-notes/release-notes-0.19.0.md +++ b/docs/release-notes/release-notes-0.19.0.md @@ -103,6 +103,18 @@ * LND updates channel.backup file at shutdown time. +* A new subsystem `chainio` is + [introduced](https://github.com/lightningnetwork/lnd/pull/9277) 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 9757c4bb520c6ae25bafe9b35693d092aadd7f3e Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 18 Oct 2024 05:35:00 +0800 Subject: [PATCH 063/153] multi: optimize loggings around changes from `blockbeat` --- chainio/dispatcher.go | 5 ++++ contractcourt/channel_arbitrator.go | 5 ++-- lnutils/log.go | 13 +++++++- sweep/fee_bumper.go | 5 +++- sweep/sweeper.go | 46 ++++++++++++++++------------- sweep/txgenerator.go | 3 +- sweep/txgenerator_test.go | 10 ++++--- 7 files changed, 56 insertions(+), 31 deletions(-) diff --git a/chainio/dispatcher.go b/chainio/dispatcher.go index 87bc21fbaa..269bb1892c 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 00edf22f09..afbb74f6a3 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.id(), lnutils.SpewLogClosure(c.activeHTLCs), state.currentState) @@ -1094,8 +1094,7 @@ func (c *ChannelArbitrator) stateStep( return StateFullyResolved, closeTx, nil } - log.Infof("ChannelArbitrator(%v): force closing "+ - "chan", c.id()) + log.Infof("ChannelArbitrator(%v): force closing chan", c.id()) // Now that we have all the actions decided for the set of // HTLC's, we'll broadcast the commitment transaction, and diff --git a/lnutils/log.go b/lnutils/log.go index a32738bdf4..128bc6fc83 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 return 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 115ae15bdf..6db8585e63 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 500fcdc2df..184b2c60ba 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,22 @@ 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 { + inps := make([]input.Input, 0, len(inputs)) + for _, in := range inputs { + inps = append(inps, in) + } + + prefix := "\n" + if len(inps) == 0 { + prefix = "" + } + + return prefix + 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 +715,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 +1252,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 +1531,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 993ee9e59d..995949b15a 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 71477bd6ec..1cceb5cb02 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:lll + expectedSummary = "0000000000000000000000000000000000000000000000000000000000000000:10 (CommitmentTimeLock)\n" + + "0000000000000000000000000000000000000000000000000000000000000001:11 (HtlcAcceptedSuccessSecondLevel)\n" + + "0000000000000000000000000000000000000000000000000000000000000002:12 (HtlcOfferedRemoteTimeout)\n" + "0000000000000000000000000000000000000000000000000000000000000003:13 (WitnessKeyHash)" ) From 9bd342e6dad478f6022becb7f61b757a361fdfe1 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 24 Oct 2024 19:50:47 +0800 Subject: [PATCH 064/153] lntest+itest: fix `testSweepCPFPAnchorOutgoingTimeout` --- itest/lnd_sweep_test.go | 129 +++++++++++++++--------------------- lntest/harness_assertion.go | 33 +++++++++ 2 files changed, 88 insertions(+), 74 deletions(-) diff --git a/itest/lnd_sweep_test.go b/itest/lnd_sweep_test.go index 17e0910b63..0d668d1789 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,52 +168,36 @@ 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, - // - 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 - // anchors because we don't know which one will be confirmed - if we - // only broadcast the local anchor sweeping, our peer can broadcast - // 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) + var anchorSweep *walletrpc.PendingSweep - // 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 + // Bob should have one pending sweep, + // - anchor sweeping from his local commitment. + 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) @@ -228,11 +209,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 +238,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 +267,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 +274,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 +296,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 +323,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 +337,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 1b079fea16..c95d8346c6 100644 --- a/lntest/harness_assertion.go +++ b/lntest/harness_assertion.go @@ -2786,3 +2786,36 @@ 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) + + // Assume the first is the force close tx. + forceCloseTx, anchorSweepTx := txns[0], txns[1] + + // Get the txid. + closeTxid := forceCloseTx.TxHash() + + // We now check whether there is an anchor input used in the assumed + // anchorSweepTx by checking every input's previous outpoint against + // the assumed closingTxid. If we fail to find one, it means the first + // item from the above txns is the anchor sweeping tx. + for _, inp := range anchorSweepTx.TxIn { + if inp.PreviousOutPoint.Hash == closeTxid { + // Found a match, this is indeed the anchor sweeping tx + // so we return it here. + return forceCloseTx, anchorSweepTx + } + } + + // The assumed order is incorrect so we swap and return. + forceCloseTx, anchorSweepTx = anchorSweepTx, forceCloseTx + + return forceCloseTx, anchorSweepTx +} From 72e17a4fa796d4faa6e248986055c5cbd5ff222a Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 24 Oct 2024 19:51:46 +0800 Subject: [PATCH 065/153] 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 0d668d1789..8f1c8f013d 100644 --- a/itest/lnd_sweep_test.go +++ b/itest/lnd_sweep_test.go @@ -385,10 +385,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 @@ -505,40 +502,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) @@ -550,11 +537,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( @@ -580,10 +566,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. @@ -592,7 +583,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)) @@ -600,13 +591,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)) @@ -614,9 +598,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, @@ -636,15 +620,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)) @@ -662,10 +648,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()) @@ -679,6 +662,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 3023c3572b13151ec93c0b7dfbc5dad111c5a430 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 24 Oct 2024 19:52:30 +0800 Subject: [PATCH 066/153] itest: fix `testSweepHTLCs` --- itest/lnd_sweep_test.go | 87 ++++++++++++++++++++++------------------- 1 file changed, 46 insertions(+), 41 deletions(-) diff --git a/itest/lnd_sweep_test.go b/itest/lnd_sweep_test.go index 8f1c8f013d..ae532eb56a 100644 --- a/itest/lnd_sweep_test.go +++ b/itest/lnd_sweep_test.go @@ -700,9 +700,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 @@ -859,34 +859,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 @@ -910,8 +911,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. @@ -936,13 +937,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 @@ -984,22 +984,33 @@ 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] + } // 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 @@ -1015,13 +1026,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. @@ -1189,8 +1193,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 79a8260f68510044f10b4801aed39373b8c7f1f5 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 24 Oct 2024 19:52:55 +0800 Subject: [PATCH 067/153] itest: fix `testSweepCommitOutputAndAnchor` --- itest/lnd_sweep_test.go | 358 ++++++++-------------------------------- 1 file changed, 72 insertions(+), 286 deletions(-) diff --git a/itest/lnd_sweep_test.go b/itest/lnd_sweep_test.go index ae532eb56a..208d1dc1ca 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" @@ -1208,10 +1207,19 @@ 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 + // 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. + deadlineA := deadline + 1 + + // 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 @@ -1222,7 +1230,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. @@ -1230,25 +1238,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. @@ -1347,155 +1337,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 { @@ -1509,20 +1385,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 @@ -1537,7 +1399,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 + @@ -1546,119 +1408,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) @@ -1681,9 +1465,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()) } @@ -1719,9 +1503,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", @@ -1737,16 +1522,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 29bb9b860465bc8db0c24ecffe73cd7840e48647 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 17 Oct 2024 08:41:22 +0800 Subject: [PATCH 068/153] 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 208d1dc1ca..5cad6269ae 100644 --- a/itest/lnd_sweep_test.go +++ b/itest/lnd_sweep_test.go @@ -1871,6 +1871,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) @@ -1896,6 +1897,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 @@ -2008,6 +2010,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 d116a561538a396d03a1b11db413be9edf241e8a Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 24 Oct 2024 16:58:02 +0800 Subject: [PATCH 069/153] 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 3515cbc946..7fb5dfa42d 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 50d8d75567780445e1402f3c89050e1bdd8ff744 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 18 Oct 2024 05:26:52 +0800 Subject: [PATCH 070/153] 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 | 151 ++++++++++ lntest/node/config.go | 34 +++ 5 files changed, 551 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 68c923c3bb..9c37dec6e1 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, @@ -707,3 +703,8 @@ var allTestCases = []*lntest.TestCase{ TestFunc: testDebuglevelShow, }, } + +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 0000000000..557a61647d --- /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 f3a906c141..f02cd79ee4 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 f96a3aadd7..e5a13f53dc 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" + "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,119 @@ 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 { + 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 1e8129e7d8..05ef272797 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 1607d4b57995864f50b79aef8ccad96eb98fc478 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 18 Oct 2024 13:42:23 +0800 Subject: [PATCH 071/153] 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 9c37dec6e1..5d60b03d97 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 557a61647d..4ba7c0e5c7 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 f02cd79ee4..e002bbebbc 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 2d3b28b9d5ed5533756d907841a55b6bcc58e64e Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 19 Oct 2024 07:35:40 +0800 Subject: [PATCH 072/153] 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 5d60b03d97..779ccdc615 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 4ba7c0e5c7..c4773be6d8 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:lll 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 e002bbebbc..2a866f7929 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 c95d8346c6..edb6387eb8 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 493650e3ecc00de38d2fbeda7179ec9cc08cd048 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 19 Oct 2024 10:37:33 +0800 Subject: [PATCH 073/153] 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 779ccdc615..76eee0eff2 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 c4773be6d8..96fb5ced5b 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 2a866f7929..e6437b053b 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 edb6387eb8..3d829b64e3 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 6c50559a7d2b5e2f3dfbfbcad8e82686cb6e9b84 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Mon, 21 Oct 2024 13:05:41 +0800 Subject: [PATCH 074/153] itest: flatten `testMultiHopHtlcLocalChainClaim` --- itest/list_on_test.go | 4 - itest/lnd_multi-hop_force_close_test.go | 672 +++++++++++++++++++++++- itest/lnd_multi-hop_test.go | 361 ------------- 3 files changed, 667 insertions(+), 370 deletions(-) diff --git a/itest/list_on_test.go b/itest/list_on_test.go index 76eee0eff2..1537d7978e 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 96fb5ced5b..59ef828cc6 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,652 @@ 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) + + // 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 e6437b053b..ef50d57a71 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 aa0ca88c9a943ce21da828b960aace456d9ec545 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Wed, 23 Oct 2024 02:30:44 +0800 Subject: [PATCH 075/153] itest: flatten `testMultiHopHtlcRemoteChainClaim` --- itest/list_on_test.go | 4 - itest/lnd_multi-hop_force_close_test.go | 632 ++++++++++++++++++++++++ itest/lnd_multi-hop_test.go | 295 ----------- 3 files changed, 632 insertions(+), 299 deletions(-) diff --git a/itest/list_on_test.go b/itest/list_on_test.go index 1537d7978e..537a3b32c3 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 59ef828cc6..3fe0853ca5 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 @@ -2070,3 +2082,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 ef50d57a71..33f91f383a 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 bdca3aff1151d5a86504ddf345c9888acc82f2dd Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Wed, 23 Oct 2024 22:51:48 +0800 Subject: [PATCH 076/153] itest: flatten `testMultiHopHtlcAggregation` --- itest/list_on_test.go | 4 - itest/lnd_multi-hop_force_close_test.go | 420 ++++++++++++++++++++++ itest/lnd_multi-hop_test.go | 457 ------------------------ 3 files changed, 420 insertions(+), 461 deletions(-) diff --git a/itest/list_on_test.go b/itest/list_on_test.go index 537a3b32c3..0752814f47 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 3fe0853ca5..48cfc20bce 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 @@ -2702,3 +2715,410 @@ 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) + + // Once bob has force closed, we can restart carol. + require.NoError(ht, restartCarol()) + + // Mine a block to confirm Bob's force close tx and anchor sweeping tx. + ht.MineBlocksAndAssertNumTxes(1, 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) + + // 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 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 33f91f383a..124118af6a 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 291919aeb171d963fe6e94096e1ca0d855af7046 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Wed, 23 Oct 2024 23:57:19 +0800 Subject: [PATCH 077/153] 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 124118af6a..ff37dae22b 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 6db372c0be2820ee601b3a47db8f062eecebcecd Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 24 Oct 2024 00:28:56 +0800 Subject: [PATCH 078/153] 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 ff37dae22b..71a44966ed 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 820a4e57dcc88d586d937dc2ec0ca5adf9e4cce4 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 24 Oct 2024 00:30:08 +0800 Subject: [PATCH 079/153] 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 5581c7ae8301ce1948b92f0d3fc3c7d2658632ff Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 24 Oct 2024 01:14:01 +0800 Subject: [PATCH 080/153] 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 7fb5dfa42d..4df148d95e 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 35aa8091ed78a38645e5f441de92c609c3411514 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 24 Oct 2024 21:31:15 +0800 Subject: [PATCH 081/153] 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 0752814f47..53fcc5c799 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 6d02804012..891eb96e4a 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 fda9ef174e65c8a51ecc56d46d26945121b90ab0 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 24 Oct 2024 21:36:35 +0800 Subject: [PATCH 082/153] 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 7daf95960d..0d8d05beea 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 70c2f3fc3a7d639cfe5bd0dbc194119dac99e578 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 24 Oct 2024 21:47:39 +0800 Subject: [PATCH 083/153] 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 438661138b..a774b3aa70 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 241da54cff12df0a1d126480885d1d611c30f608 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 24 Oct 2024 23:58:20 +0800 Subject: [PATCH 084/153] 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 5ae0df9584..25a06e3dce 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 b3a148974c0cadac9bd182d206aa91fd66e40c6b Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 29 Jun 2024 10:37:38 +0800 Subject: [PATCH 085/153] 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 | 369 +++++++++----------------- lntest/harness.go | 19 +- lntest/harness_miner.go | 3 +- 4 files changed, 156 insertions(+), 243 deletions(-) diff --git a/itest/list_on_test.go b/itest/list_on_test.go index 53fcc5c799..cbe9abb30d 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 891eb96e4a..ed21752a2f 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:lll 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,17 @@ 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) + // 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 +625,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 +643,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 +681,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 +701,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 +714,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 +743,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 e5a13f53dc..5fb4be4dca 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 22b2b95acc..17fd864ed7 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 6cf665d10f70608a71b2b1966e94bb6df011d62a Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 25 Oct 2024 14:05:18 +0800 Subject: [PATCH 086/153] 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 cbe9abb30d..20e25693d4 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, @@ -687,4 +683,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 0a36b077d2..80a0eae5a3 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 897803b73b7e5b165e84ebb7df1380b74d57d542 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 25 Oct 2024 14:05:55 +0800 Subject: [PATCH 087/153] 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 25a06e3dce..d9886ed23e 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 fd378a25f7..85690da7d3 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 a302f596a6..b6211d7f8e 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 fe7d5c18cb157b0199793ee88b0c6fff418479de Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 25 Oct 2024 03:03:06 +0800 Subject: [PATCH 088/153] 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 72dd16ea34..8ab276d2e6 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 ed21752a2f..0ef144b1e6 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 9bbecd31b3..6d6cdef7e9 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 7c616e7a50..8cdd8a5298 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 71a44966ed..cd1dd3d483 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 02e3386e6a..33a7067552 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 48cfc20bce..03bcbaa9ba 100644 --- a/itest/lnd_multi-hop_force_close_test.go +++ b/itest/lnd_multi-hop_force_close_test.go @@ -298,7 +298,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, @@ -306,9 +306,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, @@ -316,7 +317,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. @@ -645,7 +647,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. @@ -1006,7 +1008,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. @@ -1335,7 +1337,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. @@ -1594,7 +1596,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. @@ -1897,7 +1899,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. @@ -2252,7 +2254,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. @@ -2535,7 +2537,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. @@ -2982,7 +2984,7 @@ func runHtlcAggregation(ht *lntest.HarnessTest, TimeoutSeconds: 60, FeeLimitMsat: noFeeLimitMsat, } - alice.RPC.SendPayment(req) + ht.SendPaymentAssertInflight(alice, req) } // And Carol will pay Alice's. @@ -2992,7 +2994,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 80a0eae5a3..a68df852bc 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 b8da89654ea2b091bfd50588d074443a4a1da926 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 5 Nov 2024 08:17:48 +0800 Subject: [PATCH 089/153] 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 8cdd8a5298..d9b8517107 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 03bcbaa9ba..308fa9be05 100644 --- a/itest/lnd_multi-hop_force_close_test.go +++ b/itest/lnd_multi-hop_force_close_test.go @@ -320,11 +320,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 @@ -364,7 +371,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 @@ -651,9 +658,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 @@ -1010,11 +1026,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 @@ -1339,11 +1364,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 @@ -1600,9 +1632,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 @@ -1903,9 +1944,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 @@ -2257,10 +2307,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 @@ -2540,10 +2597,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 @@ -2997,11 +3061,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 3d829b64e3..5d502b5be4 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 84c8aa612b9c036126939f61b39e64d44ddff567 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sun, 10 Nov 2024 17:18:14 +0800 Subject: [PATCH 090/153] htlcswitch: handle nil circuit properly when settling We have two sources which can call `handlePacketSettle`, either through the link's `<-s.htlcPlex`, or the `<-s.resolutionMsgs`, which means the `closeCircuit` could be call twice. Previously we already caught this case inside `closeCircuit`, in that we would return a nil circuit upon seeing `ErrUnknownCircuit`, indicating the circuit was removed. However, we still need to account the case when the circuit is being closed, which is now fixed as we will ignore when seeing `ErrCircuitClosing`. --- htlcswitch/packet.go | 11 +++++++++++ htlcswitch/switch.go | 11 ++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/htlcswitch/packet.go b/htlcswitch/packet.go index 31639dd5d1..7fa5a34b21 100644 --- a/htlcswitch/packet.go +++ b/htlcswitch/packet.go @@ -1,6 +1,8 @@ package htlcswitch import ( + "fmt" + "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/htlcswitch/hop" @@ -136,3 +138,12 @@ func (p *htlcPacket) keystone() Keystone { OutKey: p.outKey(), } } + +// String returns a human-readable description of the packet. +func (p *htlcPacket) String() string { + return fmt.Sprintf("keystone=%v, sourceRef=%v, destRef=%v, "+ + "incomingAmount=%v, amount=%v, localFailure=%v, hasSource=%v "+ + "isResolution=%v", p.keystone(), p.sourceRef, p.destRef, + p.incomingAmount, p.amount, p.localFailure, p.hasSource, + p.isResolution) +} diff --git a/htlcswitch/switch.go b/htlcswitch/switch.go index cc345e461b..07ce65fc58 100644 --- a/htlcswitch/switch.go +++ b/htlcswitch/switch.go @@ -2994,6 +2994,15 @@ func (s *Switch) handlePacketSettle(packet *htlcPacket) error { // If the source of this packet has not been set, use the circuit map // to lookup the origin. circuit, err := s.closeCircuit(packet) + + // If the circuit is in the process of closing, we will return a nil as + // there's another packet handling undergoing. + if errors.Is(err, ErrCircuitClosing) { + log.Debugf("Circuit is closing for packet=%v", packet) + return nil + } + + // Exit early if there's another error. if err != nil { return err } @@ -3005,7 +3014,7 @@ func (s *Switch) handlePacketSettle(packet *htlcPacket) error { // and when `UpdateFulfillHTLC` is received. After which `RevokeAndAck` // is received, which invokes `processRemoteSettleFails` in its link. if circuit == nil { - log.Debugf("Found nil circuit: packet=%v", spew.Sdump(packet)) + log.Debugf("Circuit already closed for packet=%v", packet) return nil } From f777264eb985fae53ab0da2a858ac459623bae5f Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 8 Nov 2024 02:44:11 +0800 Subject: [PATCH 091/153] routing: fix nil pointer dereference in `exitWithErr` In a rare case when the critical log is triggered when using postgres as db backend, the `payment` could be nil cause the server is shutting down, causing the payment fetching to return an error. We now cache its state before fetching it from the db. --- routing/payment_lifecycle.go | 7 +++++-- routing/payment_lifecycle_test.go | 18 +++++++++--------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/routing/payment_lifecycle.go b/routing/payment_lifecycle.go index 93b214bdea..b94b5c3917 100644 --- a/routing/payment_lifecycle.go +++ b/routing/payment_lifecycle.go @@ -180,6 +180,9 @@ func (p *paymentLifecycle) resumePayment(ctx context.Context) ([32]byte, return [32]byte{}, nil, err } + // Get the payment state. + ps := payment.GetState() + for _, a := range payment.InFlightHTLCs() { a := a @@ -192,7 +195,7 @@ func (p *paymentLifecycle) resumePayment(ctx context.Context) ([32]byte, // exitWithErr is a helper closure that logs and returns an error. exitWithErr := func(err error) ([32]byte, *route.Route, error) { log.Errorf("Payment %v with status=%v failed: %v", - p.identifier, payment.GetStatus(), err) + p.identifier, ps, err) return [32]byte{}, nil, err } @@ -210,7 +213,7 @@ lifecycle: return exitWithErr(err) } - ps := payment.GetState() + ps = payment.GetState() remainingFees := p.calcFeeBudget(ps.FeesPaid) log.Debugf("Payment %v: status=%v, active_shards=%v, "+ diff --git a/routing/payment_lifecycle_test.go b/routing/payment_lifecycle_test.go index 315c1bad58..45d1491098 100644 --- a/routing/payment_lifecycle_test.go +++ b/routing/payment_lifecycle_test.go @@ -673,7 +673,7 @@ func TestResumePaymentFailOnTimeout(t *testing.T) { ps := &channeldb.MPPaymentState{ RemainingAmt: paymentAmt, } - m.payment.On("GetState").Return(ps).Once() + m.payment.On("GetState").Return(ps).Twice() // NOTE: GetStatus is only used to populate the logs which is not // critical, so we loosen the checks on how many times it's been called. @@ -730,7 +730,7 @@ func TestResumePaymentFailOnTimeoutErr(t *testing.T) { ps := &channeldb.MPPaymentState{ RemainingAmt: paymentAmt, } - m.payment.On("GetState").Return(ps).Once() + m.payment.On("GetState").Return(ps).Twice() // NOTE: GetStatus is only used to populate the logs which is // not critical so we loosen the checks on how many times it's @@ -773,7 +773,7 @@ func TestResumePaymentFailContextCancel(t *testing.T) { ps := &channeldb.MPPaymentState{ RemainingAmt: paymentAmt, } - m.payment.On("GetState").Return(ps).Once() + m.payment.On("GetState").Return(ps).Twice() // NOTE: GetStatus is only used to populate the logs which is not // critical, so we loosen the checks on how many times it's been called. @@ -826,7 +826,7 @@ func TestResumePaymentFailOnStepErr(t *testing.T) { ps := &channeldb.MPPaymentState{ RemainingAmt: paymentAmt, } - m.payment.On("GetState").Return(ps).Once() + m.payment.On("GetState").Return(ps).Twice() // NOTE: GetStatus is only used to populate the logs which is // not critical so we loosen the checks on how many times it's @@ -864,7 +864,7 @@ func TestResumePaymentFailOnRequestRouteErr(t *testing.T) { ps := &channeldb.MPPaymentState{ RemainingAmt: paymentAmt, } - m.payment.On("GetState").Return(ps).Once() + m.payment.On("GetState").Return(ps).Twice() // NOTE: GetStatus is only used to populate the logs which is // not critical so we loosen the checks on how many times it's @@ -910,7 +910,7 @@ func TestResumePaymentFailOnRegisterAttemptErr(t *testing.T) { ps := &channeldb.MPPaymentState{ RemainingAmt: paymentAmt, } - m.payment.On("GetState").Return(ps).Once() + m.payment.On("GetState").Return(ps).Twice() // NOTE: GetStatus is only used to populate the logs which is // not critical so we loosen the checks on how many times it's @@ -970,7 +970,7 @@ func TestResumePaymentFailOnSendAttemptErr(t *testing.T) { ps := &channeldb.MPPaymentState{ RemainingAmt: paymentAmt, } - m.payment.On("GetState").Return(ps).Once() + m.payment.On("GetState").Return(ps).Twice() // NOTE: GetStatus is only used to populate the logs which is // not critical so we loosen the checks on how many times it's @@ -1062,7 +1062,7 @@ func TestResumePaymentSuccess(t *testing.T) { ps := &channeldb.MPPaymentState{ RemainingAmt: paymentAmt, } - m.payment.On("GetState").Return(ps).Once() + m.payment.On("GetState").Return(ps).Twice() // NOTE: GetStatus is only used to populate the logs which is // not critical so we loosen the checks on how many times it's @@ -1163,7 +1163,7 @@ func TestResumePaymentSuccessWithTwoAttempts(t *testing.T) { ps := &channeldb.MPPaymentState{ RemainingAmt: paymentAmt, } - m.payment.On("GetState").Return(ps).Once() + m.payment.On("GetState").Return(ps).Twice() // NOTE: GetStatus is only used to populate the logs which is // not critical so we loosen the checks on how many times it's From 0725cb5b365d94ee4d2306bdf2dcc023af2a3dba Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 5 Nov 2024 08:20:32 +0800 Subject: [PATCH 092/153] lnwallet: add debug logs --- lnwallet/channel.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 74ecd17454..5de02a3142 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -1711,7 +1711,7 @@ func (lc *LightningChannel) restorePendingRemoteUpdates( localCommitmentHeight uint64, pendingRemoteCommit *commitment) error { - lc.log.Debugf("Restoring %v dangling remote updates", + lc.log.Debugf("Restoring %v dangling remote updates pending our sig", len(unsignedAckedUpdates)) for _, logUpdate := range unsignedAckedUpdates { @@ -1830,6 +1830,9 @@ func (lc *LightningChannel) restorePendingLocalUpdates( pendingCommit := pendingRemoteCommitDiff.Commitment pendingHeight := pendingCommit.CommitHeight + lc.log.Debugf("Restoring pending remote commitment %v at commit "+ + "height %v", pendingCommit.CommitTx.TxHash(), pendingHeight) + auxResult, err := fn.MapOptionZ( lc.leafStore, func(s AuxLeafStore) fn.Result[CommitDiffAuxResult] { From 13ef2ee0f2ed0191142cd5311d54a83e10a61a4c Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 26 Oct 2024 03:29:12 +0800 Subject: [PATCH 093/153] itest: print num of blocks for debugging --- itest/lnd_test.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/itest/lnd_test.go b/itest/lnd_test.go index 5b9105694d..cb0871b963 100644 --- a/itest/lnd_test.go +++ b/itest/lnd_test.go @@ -106,6 +106,9 @@ func TestLightningNetworkDaemon(t *testing.T) { // among all the test cases. harnessTest.SetupStandbyNodes() + // Get the current block height. + height := harnessTest.CurrentHeight() + // Run the subset of the test cases selected in this tranche. for idx, testCase := range testCases { testCase := testCase @@ -151,9 +154,10 @@ func TestLightningNetworkDaemon(t *testing.T) { } } - height := harnessTest.CurrentHeight() - t.Logf("=========> tests finished for tranche: %v, tested %d "+ - "cases, end height: %d\n", trancheIndex, len(testCases), height) + //nolint:forbidigo + fmt.Printf("=========> tranche %v finished, tested %d cases, mined "+ + "blocks: %d\n", trancheIndex, len(testCases), + harnessTest.CurrentHeight()-height) } // getTestCaseSplitTranche returns the sub slice of the test cases that should From bbc494537d5e21d9e44524c637681ca7fc342c20 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 25 Oct 2024 03:33:37 +0800 Subject: [PATCH 094/153] itest: shuffle test cases to even out blocks mined in tranches This commit adds a new flag to shuffle all the test cases before running them so tests which require lots of blocks to be mined are less likely to be run in the same tranch. The other benefit is this approach provides a more efficient way to figure which tests are broken since all the differnet backends are running different tranches in their builds, we can identify more failed tests in one push. --- Makefile | 4 ++-- itest/list_on_test.go | 4 +++- itest/lnd_test.go | 37 +++++++++++++++++++++++++++++++++++++ make/testing_flags.mk | 6 ++++++ scripts/itest_parallel.sh | 7 ++++--- scripts/itest_part.sh | 10 +++++----- 6 files changed, 57 insertions(+), 11 deletions(-) diff --git a/Makefile b/Makefile index ccf95cb902..101b9aed34 100644 --- a/Makefile +++ b/Makefile @@ -208,7 +208,7 @@ endif itest-only: db-instance @$(call print, "Running integration tests with ${backend} backend.") rm -rf itest/*.log itest/.logs-*; date - EXEC_SUFFIX=$(EXEC_SUFFIX) scripts/itest_part.sh 0 1 $(TEST_FLAGS) $(ITEST_FLAGS) -test.v + EXEC_SUFFIX=$(EXEC_SUFFIX) scripts/itest_part.sh 0 1 $(SHUFFLE_SEED) $(TEST_FLAGS) $(ITEST_FLAGS) -test.v $(COLLECT_ITEST_COVERAGE) #? itest: Build and run integration tests @@ -221,7 +221,7 @@ itest-race: build-itest-race itest-only itest-parallel: build-itest db-instance @$(call print, "Running tests") rm -rf itest/*.log itest/.logs-*; date - EXEC_SUFFIX=$(EXEC_SUFFIX) scripts/itest_parallel.sh $(ITEST_PARALLELISM) $(NUM_ITEST_TRANCHES) $(TEST_FLAGS) $(ITEST_FLAGS) + EXEC_SUFFIX=$(EXEC_SUFFIX) scripts/itest_parallel.sh $(ITEST_PARALLELISM) $(NUM_ITEST_TRANCHES) $(SHUFFLE_SEED) $(TEST_FLAGS) $(ITEST_FLAGS) $(COLLECT_ITEST_COVERAGE) #? itest-clean: Kill all running itest processes diff --git a/itest/list_on_test.go b/itest/list_on_test.go index 20e25693d4..b93807437e 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -2,7 +2,9 @@ package itest -import "github.com/lightningnetwork/lnd/lntest" +import ( + "github.com/lightningnetwork/lnd/lntest" +) var allTestCases = []*lntest.TestCase{ { diff --git a/itest/lnd_test.go b/itest/lnd_test.go index cb0871b963..f2a345fa11 100644 --- a/itest/lnd_test.go +++ b/itest/lnd_test.go @@ -20,6 +20,7 @@ import ( "github.com/lightningnetwork/lnd/lntest/port" "github.com/lightningnetwork/lnd/lntest/wait" "github.com/stretchr/testify/require" + "golang.org/x/exp/rand" "google.golang.org/grpc/grpclog" ) @@ -61,6 +62,13 @@ var ( "0-based index specified by the -runtranche flag", ) + // shuffleSeedFlag is the source of randomness used to shuffle the test + // cases. If not specified, the test cases won't be shuffled. + shuffleSeedFlag = flag.Uint64( + "shuffleseed", 0, "if set, shuffles the test cases using this "+ + "as the source of randomness", + ) + // testCasesRunTranche is the 0-based index of the split test cases // tranche to run in the current invocation. testCasesRunTranche = flag.Uint( @@ -160,6 +168,32 @@ func TestLightningNetworkDaemon(t *testing.T) { harnessTest.CurrentHeight()-height) } +// maybeShuffleTestCases shuffles the test cases if the flag `shuffleseed` is +// set and not 0. In parallel tests we want to shuffle the test cases so they +// are executed in a random order. This is done to even out the blocks mined in +// each test tranche so they can run faster. +// +// NOTE: Because the parallel tests are initialized with the same seed (job +// ID), they will always have the same order. +func maybeShuffleTestCases() { + // Exit if not set. + if shuffleSeedFlag == nil { + return + } + + // Exit if set to 0. + if *shuffleSeedFlag == 0 { + return + } + + // Init the seed and shuffle the test cases. + rand.Seed(*shuffleSeedFlag) + rand.Shuffle(len(allTestCases), func(i, j int) { + allTestCases[i], allTestCases[j] = + allTestCases[j], allTestCases[i] + }) +} + // getTestCaseSplitTranche returns the sub slice of the test cases that should // be run as the current split tranche as well as the index and slice offset of // the tranche. @@ -182,6 +216,9 @@ func getTestCaseSplitTranche() ([]*lntest.TestCase, uint, uint) { runTranche = 0 } + // Shuffle the test cases if the `shuffleseed` flag is set. + maybeShuffleTestCases() + numCases := uint(len(allTestCases)) testsPerTranche := numCases / numTranches trancheOffset := runTranche * testsPerTranche diff --git a/make/testing_flags.mk b/make/testing_flags.mk index b2db6861c3..ba5b3fb378 100644 --- a/make/testing_flags.mk +++ b/make/testing_flags.mk @@ -10,6 +10,7 @@ COVER_PKG = $$(go list -deps -tags="$(DEV_TAGS)" ./... | grep '$(PKG)' | grep -v NUM_ITEST_TRANCHES = 4 ITEST_PARALLELISM = $(NUM_ITEST_TRANCHES) POSTGRES_START_DELAY = 5 +SHUFFLE_SEED = 0 # If rpc option is set also add all extra RPC tags to DEV_TAGS ifneq ($(with-rpc),) @@ -27,6 +28,11 @@ ifneq ($(parallel),) ITEST_PARALLELISM = $(parallel) endif +# Set the seed for shuffling the test cases. +ifneq ($(shuffleseed),) +SHUFFLE_SEED = $(shuffleseed) +endif + # Windows needs to append a .exe suffix to all executable files, otherwise it # won't run them. ifneq ($(windows),) diff --git a/scripts/itest_parallel.sh b/scripts/itest_parallel.sh index 2ae7f3f531..ab1b3efa28 100755 --- a/scripts/itest_parallel.sh +++ b/scripts/itest_parallel.sh @@ -3,9 +3,10 @@ # Get all the variables. PROCESSES=$1 TRANCHES=$2 +SHUFFLE_SEED=$3 -# Here we also shift 2 times and get the rest of our flags to pass on in $@. -shift 2 +# Here we also shift 3 times and get the rest of our flags to pass on in $@. +shift 3 # Create a variable to hold the final exit code. exit_code=0 @@ -13,7 +14,7 @@ exit_code=0 # Run commands using xargs in parallel and capture their PIDs pids=() for ((i=0; i Date: Thu, 7 Nov 2024 12:50:40 +0800 Subject: [PATCH 095/153] workflows: pass action ID as the shuffle seed To make sure each run is shuffled, we use the action ID as the seed. --- .github/workflows/main.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0f101f866d..38de5d78be 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -284,7 +284,7 @@ jobs: run: ./scripts/install_bitcoind.sh $BITCOIN_VERSION - name: run ${{ matrix.name }} - run: make itest-parallel tranches=${{ env.TRANCHES }} ${{ matrix.args }} + run: make itest-parallel tranches=${{ env.TRANCHES }} ${{ matrix.args }} shuffleseed=${{ github.run_id }}${{ strategy.job-index }} - name: Send coverage if: ${{ contains(matrix.args, 'cover=1') }} @@ -332,7 +332,7 @@ jobs: key-prefix: integration-test - name: run itest - run: make itest-parallel tranches=${{ env.TRANCHES }} windows=1 + run: make itest-parallel tranches=${{ env.TRANCHES }} windows=1 shuffleseed=${{ github.run_id }} - name: kill any remaining lnd processes if: ${{ failure() }} @@ -382,7 +382,7 @@ jobs: mv bitcoin-${BITCOIN_VERSION}.0 /tmp/bitcoin - name: run itest - run: PATH=$PATH:/tmp/bitcoin/bin make itest-parallel tranches=${{ env.TRANCHES }} backend=bitcoind + run: PATH=$PATH:/tmp/bitcoin/bin make itest-parallel tranches=${{ env.TRANCHES }} backend=bitcoind shuffleseed=${{ github.run_id }} - name: Zip log files on failure if: ${{ failure() }} From cffea2f494d43b50983ed4baf3321c6fd81ac8b5 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 26 Oct 2024 16:24:44 +0800 Subject: [PATCH 096/153] itest: remove direct reference to stanby nodes Prepares the upcoming refactor. We now never call `ht.Alice` directly, instead, we always init `alice := ht.Alice` so it's easier to see how they are removed in a following commit. --- itest/lnd_custom_features.go | 22 +++++----- itest/lnd_estimate_route_fee_test.go | 2 +- itest/lnd_funding_test.go | 6 ++- itest/lnd_misc_test.go | 4 +- itest/lnd_neutrino_test.go | 8 ++-- itest/lnd_onchain_test.go | 20 ++++++--- itest/lnd_open_channel_test.go | 4 +- itest/lnd_psbt_test.go | 41 +++++++++-------- itest/lnd_rest_api_test.go | 4 +- itest/lnd_route_blinding_test.go | 66 ++++++++++++++++------------ itest/lnd_routing_test.go | 12 ++--- itest/lnd_signer_test.go | 12 +++-- itest/lnd_sweep_test.go | 4 +- itest/lnd_taproot_test.go | 34 +++++++------- itest/lnd_watchtower_test.go | 22 ++++++---- 15 files changed, 155 insertions(+), 106 deletions(-) diff --git a/itest/lnd_custom_features.go b/itest/lnd_custom_features.go index 4867ca2d11..c7cec8f1dd 100644 --- a/itest/lnd_custom_features.go +++ b/itest/lnd_custom_features.go @@ -13,6 +13,8 @@ import ( // sets. For completeness, it also asserts that features aren't set in places // where they aren't intended to be. func testCustomFeatures(ht *lntest.HarnessTest) { + alice, bob := ht.Alice, ht.Bob + var ( // Odd custom features so that we don't need to worry about // issues connecting to peers. @@ -27,20 +29,20 @@ func testCustomFeatures(ht *lntest.HarnessTest) { fmt.Sprintf("--protocol.custom-nodeann=%v", customNodeAnn), fmt.Sprintf("--protocol.custom-invoice=%v", customInvoice), } - ht.RestartNodeWithExtraArgs(ht.Alice, extraArgs) + ht.RestartNodeWithExtraArgs(alice, extraArgs) // Connect nodes and open a channel so that Alice will be included // in Bob's graph. - ht.ConnectNodes(ht.Alice, ht.Bob) + ht.ConnectNodes(alice, bob) chanPoint := ht.OpenChannel( - ht.Alice, ht.Bob, lntest.OpenChannelParams{Amt: 1000000}, + alice, bob, lntest.OpenChannelParams{Amt: 1000000}, ) // Check that Alice's custom feature bit was sent to Bob in her init // message. - peers := ht.Bob.RPC.ListPeers() + peers := bob.RPC.ListPeers() require.Len(ht, peers.Peers, 1) - require.Equal(ht, peers.Peers[0].PubKey, ht.Alice.PubKeyStr) + require.Equal(ht, peers.Peers[0].PubKey, alice.PubKeyStr) _, customInitSet := peers.Peers[0].Features[customInit] require.True(ht, customInitSet) @@ -51,7 +53,7 @@ func testCustomFeatures(ht *lntest.HarnessTest) { // Assert that Alice's custom feature bit is contained in the node // announcement sent to Bob. - updates := ht.AssertNumNodeAnns(ht.Bob, ht.Alice.PubKeyStr, 1) + updates := ht.AssertNumNodeAnns(bob, alice.PubKeyStr, 1) features := updates[len(updates)-1].Features _, customFeature := features[customNodeAnn] require.True(ht, customFeature) @@ -60,8 +62,8 @@ func testCustomFeatures(ht *lntest.HarnessTest) { ) // Assert that Alice's custom feature bit is included in invoices. - invoice := ht.Alice.RPC.AddInvoice(&lnrpc.Invoice{}) - payReq := ht.Alice.RPC.DecodePayReq(invoice.PaymentRequest) + invoice := alice.RPC.AddInvoice(&lnrpc.Invoice{}) + payReq := alice.RPC.DecodePayReq(invoice.PaymentRequest) _, customInvoiceSet := payReq.Features[customInvoice] require.True(ht, customInvoiceSet) assertFeatureNotInSet( @@ -79,9 +81,9 @@ func testCustomFeatures(ht *lntest.HarnessTest) { }, }, } - ht.Alice.RPC.UpdateNodeAnnouncementErr(nodeAnnReq) + alice.RPC.UpdateNodeAnnouncementErr(nodeAnnReq) - ht.CloseChannel(ht.Alice, chanPoint) + ht.CloseChannel(alice, chanPoint) } // assertFeatureNotInSet checks that the features provided aren't contained in diff --git a/itest/lnd_estimate_route_fee_test.go b/itest/lnd_estimate_route_fee_test.go index 8ed0be2725..d8fbcca734 100644 --- a/itest/lnd_estimate_route_fee_test.go +++ b/itest/lnd_estimate_route_fee_test.go @@ -109,7 +109,7 @@ func testEstimateRouteFee(ht *lntest.HarnessTest) { }, ) - bobsPrivChannels := ht.Bob.RPC.ListChannels(&lnrpc.ListChannelsRequest{ + bobsPrivChannels := mts.bob.RPC.ListChannels(&lnrpc.ListChannelsRequest{ PrivateOnly: true, }) require.Len(ht, bobsPrivChannels.Channels, 1) diff --git a/itest/lnd_funding_test.go b/itest/lnd_funding_test.go index 0d8d05beea..f9888336d7 100644 --- a/itest/lnd_funding_test.go +++ b/itest/lnd_funding_test.go @@ -407,14 +407,16 @@ func testUnconfirmedChannelFunding(ht *lntest.HarnessTest) { // testChannelFundingInputTypes tests that any type of supported input type can // be used to fund channels. func testChannelFundingInputTypes(ht *lntest.HarnessTest) { + alice := ht.Alice + // We'll start off by creating a node for Carol. carol := ht.NewNode("Carol", nil) // Now, we'll connect her to Alice so that they can open a // channel together. - ht.ConnectNodes(carol, ht.Alice) + ht.ConnectNodes(carol, alice) - runChannelFundingInputTypes(ht, ht.Alice, carol) + runChannelFundingInputTypes(ht, alice, carol) } // runChannelFundingInputTypes tests that any type of supported input type can diff --git a/itest/lnd_misc_test.go b/itest/lnd_misc_test.go index 33a7067552..21873109ff 100644 --- a/itest/lnd_misc_test.go +++ b/itest/lnd_misc_test.go @@ -763,6 +763,8 @@ func testAbandonChannel(ht *lntest.HarnessTest) { // testSendAllCoins tests that we're able to properly sweep all coins from the // wallet into a single target address at the specified fee rate. func testSendAllCoins(ht *lntest.HarnessTest) { + alice := ht.Alice + // First, we'll make a new node, Ainz who'll we'll use to test wallet // sweeping. // @@ -789,7 +791,7 @@ func testSendAllCoins(ht *lntest.HarnessTest) { // Ensure that we can't send coins to another user's Pubkey. err = ainz.RPC.SendCoinsAssertErr(&lnrpc.SendCoinsRequest{ - Addr: ht.Alice.RPC.GetInfo().IdentityPubkey, + Addr: alice.RPC.GetInfo().IdentityPubkey, SendAll: true, Label: sendCoinsLabel, TargetConf: 6, diff --git a/itest/lnd_neutrino_test.go b/itest/lnd_neutrino_test.go index 2c551362a2..39531bb84c 100644 --- a/itest/lnd_neutrino_test.go +++ b/itest/lnd_neutrino_test.go @@ -13,8 +13,10 @@ func testNeutrino(ht *lntest.HarnessTest) { ht.Skipf("skipping test for non neutrino backends") } + alice := ht.Alice + // Check if the neutrino sub server is running. - statusRes := ht.Alice.RPC.Status(nil) + statusRes := alice.RPC.Status(nil) require.True(ht, statusRes.Active) require.Len(ht, statusRes.Peers, 1, "unable to find a peer") @@ -22,11 +24,11 @@ func testNeutrino(ht *lntest.HarnessTest) { cFilterReq := &neutrinorpc.GetCFilterRequest{ Hash: statusRes.GetBlockHash(), } - ht.Alice.RPC.GetCFilter(cFilterReq) + alice.RPC.GetCFilter(cFilterReq) // Try to reconnect to a connected peer. addPeerReq := &neutrinorpc.AddPeerRequest{ PeerAddrs: statusRes.Peers[0], } - ht.Alice.RPC.AddPeer(addPeerReq) + alice.RPC.AddPeer(addPeerReq) } diff --git a/itest/lnd_onchain_test.go b/itest/lnd_onchain_test.go index a34b9a2e47..704676d56b 100644 --- a/itest/lnd_onchain_test.go +++ b/itest/lnd_onchain_test.go @@ -33,8 +33,10 @@ func testChainKit(ht *lntest.HarnessTest) { // testChainKitGetBlock ensures that given a block hash, the RPC endpoint // returns the correct target block. func testChainKitGetBlock(ht *lntest.HarnessTest) { + alice := ht.Alice + // Get best block hash. - bestBlockRes := ht.Alice.RPC.GetBestBlock(nil) + bestBlockRes := alice.RPC.GetBestBlock(nil) var bestBlockHash chainhash.Hash err := bestBlockHash.SetBytes(bestBlockRes.BlockHash) @@ -44,7 +46,7 @@ func testChainKitGetBlock(ht *lntest.HarnessTest) { getBlockReq := &chainrpc.GetBlockRequest{ BlockHash: bestBlockHash[:], } - getBlockRes := ht.Alice.RPC.GetBlock(getBlockReq) + getBlockRes := alice.RPC.GetBlock(getBlockReq) // Deserialize the block which was retrieved by hash. msgBlock := &wire.MsgBlock{} @@ -61,8 +63,10 @@ func testChainKitGetBlock(ht *lntest.HarnessTest) { // testChainKitGetBlockHeader ensures that given a block hash, the RPC endpoint // returns the correct target block header. func testChainKitGetBlockHeader(ht *lntest.HarnessTest) { + alice := ht.Alice + // Get best block hash. - bestBlockRes := ht.Alice.RPC.GetBestBlock(nil) + bestBlockRes := alice.RPC.GetBestBlock(nil) var ( bestBlockHash chainhash.Hash @@ -76,7 +80,7 @@ func testChainKitGetBlockHeader(ht *lntest.HarnessTest) { getBlockReq := &chainrpc.GetBlockRequest{ BlockHash: bestBlockHash[:], } - getBlockRes := ht.Alice.RPC.GetBlock(getBlockReq) + getBlockRes := alice.RPC.GetBlock(getBlockReq) // Deserialize the block which was retrieved by hash. blockReader := bytes.NewReader(getBlockRes.RawBlock) @@ -87,7 +91,7 @@ func testChainKitGetBlockHeader(ht *lntest.HarnessTest) { getBlockHeaderReq := &chainrpc.GetBlockHeaderRequest{ BlockHash: bestBlockHash[:], } - getBlockHeaderRes := ht.Alice.RPC.GetBlockHeader(getBlockHeaderReq) + getBlockHeaderRes := alice.RPC.GetBlockHeader(getBlockHeaderReq) // Deserialize the block header which was retrieved by hash. blockHeaderReader := bytes.NewReader(getBlockHeaderRes.RawBlockHeader) @@ -104,14 +108,16 @@ func testChainKitGetBlockHeader(ht *lntest.HarnessTest) { // testChainKitGetBlockHash ensures that given a block height, the RPC endpoint // returns the correct target block hash. func testChainKitGetBlockHash(ht *lntest.HarnessTest) { + alice := ht.Alice + // Get best block hash. - bestBlockRes := ht.Alice.RPC.GetBestBlock(nil) + bestBlockRes := alice.RPC.GetBestBlock(nil) // Retrieve the block hash at best block height. req := &chainrpc.GetBlockHashRequest{ BlockHeight: int64(bestBlockRes.BlockHeight), } - getBlockHashRes := ht.Alice.RPC.GetBlockHash(req) + getBlockHashRes := alice.RPC.GetBlockHash(req) // Ensure best block hash is the same as retrieved block hash. expected := bestBlockRes.BlockHash diff --git a/itest/lnd_open_channel_test.go b/itest/lnd_open_channel_test.go index a4134abcaf..b47a766d53 100644 --- a/itest/lnd_open_channel_test.go +++ b/itest/lnd_open_channel_test.go @@ -315,7 +315,9 @@ func testOpenChannelUpdateFeePolicy(ht *lntest.HarnessTest) { // closing, and ensures that if a node is subscribed to channel updates they // will be received correctly for both cooperative and force closed channels. func testBasicChannelCreationAndUpdates(ht *lntest.HarnessTest) { - runBasicChannelCreationAndUpdates(ht, ht.Alice, ht.Bob) + alice, bob := ht.Alice, ht.Bob + + runBasicChannelCreationAndUpdates(ht, alice, bob) } // runBasicChannelCreationAndUpdates tests multiple channel opening and closing, diff --git a/itest/lnd_psbt_test.go b/itest/lnd_psbt_test.go index a774b3aa70..deecbdbf22 100644 --- a/itest/lnd_psbt_test.go +++ b/itest/lnd_psbt_test.go @@ -1088,6 +1088,8 @@ func runFundAndSignPsbt(ht *lntest.HarnessTest, alice *node.HarnessNode) { // a PSBT that already specifies an input but where the user still wants the // wallet to perform coin selection. func testFundPsbt(ht *lntest.HarnessTest) { + alice, bob := ht.Alice, ht.Bob + // We test a pay-join between Alice and Bob. Bob wants to send Alice // 5 million Satoshis in a non-obvious way. So Bob selects a UTXO that's // bigger than 5 million Satoshis and expects the change minus the send @@ -1095,20 +1097,20 @@ func testFundPsbt(ht *lntest.HarnessTest) { // combines her change with the 5 million Satoshis from Bob. With this // Alice ends up paying the fees for a transfer to her. const sendAmount = 5_000_000 - aliceAddr := ht.Alice.RPC.NewAddress(&lnrpc.NewAddressRequest{ + aliceAddr := alice.RPC.NewAddress(&lnrpc.NewAddressRequest{ Type: lnrpc.AddressType_TAPROOT_PUBKEY, }) - bobAddr := ht.Bob.RPC.NewAddress(&lnrpc.NewAddressRequest{ + bobAddr := bob.RPC.NewAddress(&lnrpc.NewAddressRequest{ Type: lnrpc.AddressType_TAPROOT_PUBKEY, }) - ht.Alice.UpdateState() - ht.Bob.UpdateState() - aliceStartBalance := ht.Alice.State.Wallet.TotalBalance - bobStartBalance := ht.Bob.State.Wallet.TotalBalance + alice.UpdateState() + bob.UpdateState() + aliceStartBalance := alice.State.Wallet.TotalBalance + bobStartBalance := bob.State.Wallet.TotalBalance var bobUtxo *lnrpc.Utxo - bobUnspent := ht.Bob.RPC.ListUnspent(&walletrpc.ListUnspentRequest{}) + bobUnspent := bob.RPC.ListUnspent(&walletrpc.ListUnspentRequest{}) for _, utxo := range bobUnspent.Utxos { if utxo.AmountSat > sendAmount { bobUtxo = utxo @@ -1145,7 +1147,7 @@ func testFundPsbt(ht *lntest.HarnessTest) { require.NoError(ht, err) derivation, trDerivation := getAddressBip32Derivation( - ht, bobUtxo.Address, ht.Bob, + ht, bobUtxo.Address, bob, ) bobUtxoPkScript, _ := hex.DecodeString(bobUtxo.PkScript) @@ -1165,31 +1167,31 @@ func testFundPsbt(ht *lntest.HarnessTest) { // We have the template now. Bob basically funds the 5 million Sats to // send to Alice and Alice now only needs to coin select to pay for the // fees. - fundedPacket := fundPsbtCoinSelect(ht, ht.Alice, packet, 1) + fundedPacket := fundPsbtCoinSelect(ht, alice, packet, 1) txFee, err := fundedPacket.GetTxFee() require.NoError(ht, err) // We now let Bob sign the transaction. - signedPacket := signPacket(ht, ht.Bob, fundedPacket) + signedPacket := signPacket(ht, bob, fundedPacket) // And then Alice, which should give us a fully signed TX. - signedPacket = signPacket(ht, ht.Alice, signedPacket) + signedPacket = signPacket(ht, alice, signedPacket) // We should be able to finalize the PSBT and extract the final TX now. - extractPublishAndMine(ht, ht.Alice, signedPacket) + extractPublishAndMine(ht, alice, signedPacket) // Make sure the new wallet balances are reflected correctly. ht.AssertActiveNodesSynced() - ht.Alice.UpdateState() - ht.Bob.UpdateState() + alice.UpdateState() + bob.UpdateState() require.Equal( ht, aliceStartBalance+sendAmount-int64(txFee), - ht.Alice.State.Wallet.TotalBalance, + alice.State.Wallet.TotalBalance, ) require.Equal( ht, bobStartBalance-sendAmount, - ht.Bob.State.Wallet.TotalBalance, + bob.State.Wallet.TotalBalance, ) } @@ -1596,6 +1598,9 @@ func sendAllCoinsToAddrType(ht *lntest.HarnessTest, // the channel opening. The psbt funding flow is used to simulate this behavior // because we can easily let the remote peer run into the timeout. func testPsbtChanFundingFailFlow(ht *lntest.HarnessTest) { + alice := ht.Alice + bob := ht.Bob + const chanSize = funding.MaxBtcFundingAmount // Decrease the timeout window for the remote peer to accelerate the @@ -1604,12 +1609,10 @@ func testPsbtChanFundingFailFlow(ht *lntest.HarnessTest) { "--dev.reservationtimeout=1s", "--dev.zombiesweeperinterval=1s", } - ht.RestartNodeWithExtraArgs(ht.Bob, args) + ht.RestartNodeWithExtraArgs(bob, args) // Before we start the test, we'll ensure both sides are connected so // the funding flow can be properly executed. - alice := ht.Alice - bob := ht.Bob ht.EnsureConnected(alice, bob) // At this point, we can begin our PSBT channel funding workflow. We'll diff --git a/itest/lnd_rest_api_test.go b/itest/lnd_rest_api_test.go index ce2884e776..42a4783c21 100644 --- a/itest/lnd_rest_api_test.go +++ b/itest/lnd_rest_api_test.go @@ -237,6 +237,8 @@ func testRestAPI(ht *lntest.HarnessTest) { } func wsTestCaseSubscription(ht *lntest.HarnessTest) { + alice := ht.Alice + // Find out the current best block so we can subscribe to the next one. hash, height := ht.GetBestBlock() @@ -246,7 +248,7 @@ func wsTestCaseSubscription(ht *lntest.HarnessTest) { Height: uint32(height), } url := "/v2/chainnotifier/register/blocks" - c, err := openWebSocket(ht.Alice, url, "POST", req, nil) + c, err := openWebSocket(alice, url, "POST", req, nil) require.NoError(ht, err, "websocket") defer func() { err := c.WriteMessage(websocket.CloseMessage, closeMsg) diff --git a/itest/lnd_route_blinding_test.go b/itest/lnd_route_blinding_test.go index 85690da7d3..f09d5dc35d 100644 --- a/itest/lnd_route_blinding_test.go +++ b/itest/lnd_route_blinding_test.go @@ -556,22 +556,24 @@ func (b *blindedForwardTest) drainCarolLiquidity(incoming bool) { func setupFourHopNetwork(ht *lntest.HarnessTest, carol, dave *node.HarnessNode) []*lnrpc.ChannelPoint { + alice, bob := ht.Alice, ht.Bob + const chanAmt = btcutil.Amount(100000) var networkChans []*lnrpc.ChannelPoint // Open a channel with 100k satoshis between Alice and Bob with Alice // being the sole funder of the channel. chanPointAlice := ht.OpenChannel( - ht.Alice, ht.Bob, lntest.OpenChannelParams{ + alice, bob, lntest.OpenChannelParams{ Amt: chanAmt, }, ) networkChans = append(networkChans, chanPointAlice) // Create a channel between bob and carol. - ht.EnsureConnected(ht.Bob, carol) + ht.EnsureConnected(bob, carol) chanPointBob := ht.OpenChannel( - ht.Bob, carol, lntest.OpenChannelParams{ + bob, carol, lntest.OpenChannelParams{ Amt: chanAmt, }, ) @@ -590,7 +592,7 @@ func setupFourHopNetwork(ht *lntest.HarnessTest, networkChans = append(networkChans, chanPointCarol) // Wait for all nodes to have seen all channels. - nodes := []*node.HarnessNode{ht.Alice, ht.Bob, carol, dave} + nodes := []*node.HarnessNode{alice, bob, carol, dave} for _, chanPoint := range networkChans { for _, node := range nodes { ht.AssertChannelInGraph(node, chanPoint) @@ -609,6 +611,8 @@ func setupFourHopNetwork(ht *lntest.HarnessTest, // path and forward payments in a blinded route and finally, receiving the // payment. func testBlindedRouteInvoices(ht *lntest.HarnessTest) { + alice := ht.Alice + ctx, testCase := newBlindedForwardTest(ht) defer testCase.cleanup() @@ -634,7 +638,7 @@ func testBlindedRouteInvoices(ht *lntest.HarnessTest) { }) // Now let Alice pay the invoice. - ht.CompletePaymentRequests(ht.Alice, []string{invoice.PaymentRequest}) + ht.CompletePaymentRequests(alice, []string{invoice.PaymentRequest}) // Let Dave add a blinded invoice. // Once again let Dave create a blinded invoice. @@ -661,7 +665,7 @@ func testBlindedRouteInvoices(ht *lntest.HarnessTest) { require.EqualValues(ht, path.IntroductionNode, testCase.dave.PubKey[:]) // Now let Alice pay the invoice. - ht.CompletePaymentRequests(ht.Alice, []string{invoice.PaymentRequest}) + ht.CompletePaymentRequests(alice, []string{invoice.PaymentRequest}) } // testReceiverBlindedError tests handling of errors from the receiving node in @@ -733,6 +737,8 @@ func testRelayingBlindedError(ht *lntest.HarnessTest) { // over Alice -- Bob -- Carol -- Dave, where Bob is the introduction node and // has insufficient outgoing liquidity to forward on to carol. func testIntroductionNodeError(ht *lntest.HarnessTest) { + bob := ht.Bob + ctx, testCase := newBlindedForwardTest(ht) defer testCase.cleanup() testCase.setupNetwork(ctx, false) @@ -746,7 +752,7 @@ func testIntroductionNodeError(ht *lntest.HarnessTest) { // Subscribe to Bob's HTLC events so that we can observe the payment // coming in. - bobEvents := ht.Bob.RPC.SubscribeHtlcEvents() + bobEvents := bob.RPC.SubscribeHtlcEvents() // Once subscribed, the first event will be UNKNOWN. ht.AssertHtlcEventType(bobEvents, routerrpc.HtlcEvent_UNKNOWN) @@ -764,6 +770,8 @@ func testIntroductionNodeError(ht *lntest.HarnessTest) { // testDisableIntroductionNode tests disabling of blinded forwards for the // introduction node. func testDisableIntroductionNode(ht *lntest.HarnessTest) { + alice, bob := ht.Alice, ht.Bob + // First construct a blinded route while Bob is still advertising the // route blinding feature bit to ensure that Bob is included in the // blinded path that Dave selects. @@ -774,10 +782,10 @@ func testDisableIntroductionNode(ht *lntest.HarnessTest) { route := testCase.createRouteToBlinded(10_000_000, blindedPaymentPath) // Now, disable route blinding for Bob, then re-connect to Alice. - ht.RestartNodeWithExtraArgs(ht.Bob, []string{ + ht.RestartNodeWithExtraArgs(bob, []string{ "--protocol.no-route-blinding", }) - ht.EnsureConnected(ht.Alice, ht.Bob) + ht.EnsureConnected(alice, bob) // Assert that this fails. testCase.sendToRoute(route, false) @@ -788,6 +796,8 @@ func testDisableIntroductionNode(ht *lntest.HarnessTest) { // to resolve blinded HTLCs on chain between restarts, as we've got all the // infrastructure in place already for error testing. func testErrorHandlingOnChainFailure(ht *lntest.HarnessTest) { + alice, bob := ht.Alice, ht.Bob + // Setup a test case, note that we don't use its built in clean up // because we're going to close a channel, so we'll close out the // rest manually. @@ -807,8 +817,8 @@ func testErrorHandlingOnChainFailure(ht *lntest.HarnessTest) { // Wait for the HTLC to be active on Alice and Bob's channels. hash := sha256.Sum256(testCase.preimage[:]) - ht.AssertOutgoingHTLCActive(ht.Alice, testCase.channels[0], hash[:]) - ht.AssertOutgoingHTLCActive(ht.Bob, testCase.channels[1], hash[:]) + ht.AssertOutgoingHTLCActive(alice, testCase.channels[0], hash[:]) + ht.AssertOutgoingHTLCActive(bob, testCase.channels[1], hash[:]) // Intercept the forward on Carol's link, but do not take any action // so that we have the chance to force close with this HTLC in flight. @@ -817,11 +827,11 @@ func testErrorHandlingOnChainFailure(ht *lntest.HarnessTest) { // Force close Bob <-> Carol. closeStream, _ := ht.CloseChannelAssertPending( - ht.Bob, testCase.channels[1], true, + bob, testCase.channels[1], true, ) ht.AssertStreamChannelForceClosed( - ht.Bob, testCase.channels[1], false, closeStream, + bob, testCase.channels[1], false, closeStream, ) // SuspendCarol so that she can't interfere with the resolution of the @@ -831,32 +841,32 @@ func testErrorHandlingOnChainFailure(ht *lntest.HarnessTest) { // Mine blocks so that Bob will claim his CSV delayed local commitment, // 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.AssertNumPendingSweeps(bob, 1) ht.MineBlocksAndAssertNumTxes(1, 1) // Restart bob so that we can test that he's able to recover everything // he needs to claim a blinded HTLC. - ht.RestartNode(ht.Bob) + ht.RestartNode(bob) // Mine enough blocks for Bob to trigger timeout of his outgoing HTLC. // Carol's incoming expiry height is Bob's outgoing so we can use this // value. - info := ht.Bob.RPC.GetInfo() + info := bob.RPC.GetInfo() target := carolHTLC.IncomingExpiry - info.BlockHeight ht.MineBlocks(int(target)) // Wait for Bob's timeout transaction in the mempool, since we've // suspended Carol we don't need to account for her commitment output // claim. - ht.AssertNumPendingSweeps(ht.Bob, 0) + ht.AssertNumPendingSweeps(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[:]) + ht.AssertHTLCNotActive(bob, testCase.channels[0], hash[:]) + ht.AssertHTLCNotActive(alice, testCase.channels[0], hash[:]) // Wait for the HTLC to reflect as failed for Alice. - paymentStream := ht.Alice.RPC.TrackPaymentV2(hash[:]) + paymentStream := alice.RPC.TrackPaymentV2(hash[:]) htlcs := ht.ReceiveTrackPayment(paymentStream).Htlcs require.Len(ht, htlcs, 1) require.NotNil(ht, htlcs[0].Failure) @@ -868,7 +878,7 @@ func testErrorHandlingOnChainFailure(ht *lntest.HarnessTest) { // Clean up the rest of our force close: mine blocks so that Bob's CSV // expires to trigger his sweep and then mine it. ht.MineBlocks(node.DefaultCSV) - ht.AssertNumPendingSweeps(ht.Bob, 1) + ht.AssertNumPendingSweeps(bob, 1) ht.MineBlocksAndAssertNumTxes(1, 1) // Bring carol back up so that we can close out the rest of our @@ -885,7 +895,7 @@ func testErrorHandlingOnChainFailure(ht *lntest.HarnessTest) { // Manually close out the rest of our channels and cancel (don't use // built in cleanup which will try close the already-force-closed // channel). - ht.CloseChannel(ht.Alice, testCase.channels[0]) + ht.CloseChannel(alice, testCase.channels[0]) ht.CloseChannel(testCase.carol, testCase.channels[2]) testCase.cancel() } @@ -1191,7 +1201,7 @@ func testBlindedRouteDummyHops(ht *lntest.HarnessTest) { // Now let Alice pay the invoice. ht.CompletePaymentRequests( - ht.Alice, []string{invoiceResp.PaymentRequest}, + alice, []string{invoiceResp.PaymentRequest}, ) // Make sure Dave show the invoice as settled. @@ -1233,7 +1243,7 @@ func testBlindedRouteDummyHops(ht *lntest.HarnessTest) { // Now let Alice pay the invoice. ht.CompletePaymentRequests( - ht.Alice, []string{invoiceResp.PaymentRequest}, + alice, []string{invoiceResp.PaymentRequest}, ) // Make sure Dave show the invoice as settled. @@ -1430,6 +1440,8 @@ func testMPPToMultipleBlindedPaths(ht *lntest.HarnessTest) { // UpdateAddHTLC which we need to ensure gets included in the message on // restart. func testBlindedPaymentHTLCReForward(ht *lntest.HarnessTest) { + alice, bob := ht.Alice, ht.Bob + // Setup a test case. ctx, testCase := newBlindedForwardTest(ht) defer testCase.cleanup() @@ -1464,8 +1476,8 @@ func testBlindedPaymentHTLCReForward(ht *lntest.HarnessTest) { }() // Wait for the HTLC to be active on Alice and Bob's channels. - ht.AssertOutgoingHTLCActive(ht.Alice, testCase.channels[0], hash[:]) - ht.AssertOutgoingHTLCActive(ht.Bob, testCase.channels[1], hash[:]) + ht.AssertOutgoingHTLCActive(alice, testCase.channels[0], hash[:]) + ht.AssertOutgoingHTLCActive(bob, testCase.channels[1], hash[:]) // Intercept the forward on Carol's link. At this point, we know she // has received the HTLC and so will persist this packet. @@ -1493,7 +1505,7 @@ func testBlindedPaymentHTLCReForward(ht *lntest.HarnessTest) { // Nodes need to be connected otherwise the forwarding of the // intercepted htlc will fail. - ht.EnsureConnected(ht.Bob, testCase.carol) + ht.EnsureConnected(bob, testCase.carol) ht.EnsureConnected(testCase.carol, testCase.dave) // Now that carol and dave are connected signal the forwarding of the diff --git a/itest/lnd_routing_test.go b/itest/lnd_routing_test.go index 07adcf559c..04500a47cd 100644 --- a/itest/lnd_routing_test.go +++ b/itest/lnd_routing_test.go @@ -750,6 +750,8 @@ func testInvoiceRoutingHints(ht *lntest.HarnessTest) { // testScidAliasRoutingHints tests that dynamically created aliases via the RPC // are properly used when routing. func testScidAliasRoutingHints(ht *lntest.HarnessTest) { + bob := ht.Bob + const chanAmt = btcutil.Amount(800000) // Option-scid-alias is opt-in, as is anchors. @@ -866,8 +868,8 @@ func testScidAliasRoutingHints(ht *lntest.HarnessTest) { }) // Connect the existing Bob node with Carol via a public channel. - ht.ConnectNodes(ht.Bob, carol) - chanPointBC := ht.OpenChannel(ht.Bob, carol, lntest.OpenChannelParams{ + ht.ConnectNodes(bob, carol) + chanPointBC := ht.OpenChannel(bob, carol, lntest.OpenChannelParams{ Amt: chanAmt, PushAmt: chanAmt / 2, }) @@ -902,7 +904,7 @@ func testScidAliasRoutingHints(ht *lntest.HarnessTest) { // Now Alice will try to pay to that payment request. timeout := time.Second * 15 - stream := ht.Bob.RPC.SendPayment(&routerrpc.SendPaymentRequest{ + stream := bob.RPC.SendPayment(&routerrpc.SendPaymentRequest{ PaymentRequest: payReq, TimeoutSeconds: int32(timeout.Seconds()), FeeLimitSat: math.MaxInt64, @@ -924,7 +926,7 @@ func testScidAliasRoutingHints(ht *lntest.HarnessTest) { AliasMaps: ephemeralAliasMap, }) payReq2 := dave.RPC.AddInvoice(invoice).PaymentRequest - stream2 := ht.Bob.RPC.SendPayment(&routerrpc.SendPaymentRequest{ + stream2 := bob.RPC.SendPayment(&routerrpc.SendPaymentRequest{ PaymentRequest: payReq2, TimeoutSeconds: int32(timeout.Seconds()), FeeLimitSat: math.MaxInt64, @@ -932,7 +934,7 @@ func testScidAliasRoutingHints(ht *lntest.HarnessTest) { ht.AssertPaymentStatusFromStream(stream2, lnrpc.Payment_FAILED) ht.CloseChannel(carol, chanPointCD) - ht.CloseChannel(ht.Bob, chanPointBC) + ht.CloseChannel(bob, chanPointBC) } // testMultiHopOverPrivateChannels tests that private channels can be used as diff --git a/itest/lnd_signer_test.go b/itest/lnd_signer_test.go index 9310699887..2858e771c5 100644 --- a/itest/lnd_signer_test.go +++ b/itest/lnd_signer_test.go @@ -25,7 +25,9 @@ import ( // the node's pubkey and a customized public key to check the validity of the // result. func testDeriveSharedKey(ht *lntest.HarnessTest) { - runDeriveSharedKey(ht, ht.Alice) + alice := ht.Alice + + runDeriveSharedKey(ht, alice) } // runDeriveSharedKey checks the ECDH performed by the endpoint @@ -197,7 +199,9 @@ func runDeriveSharedKey(ht *lntest.HarnessTest, alice *node.HarnessNode) { // testSignOutputRaw makes sure that the SignOutputRaw RPC can be used with all // custom ways of specifying the signing key in the key descriptor/locator. func testSignOutputRaw(ht *lntest.HarnessTest) { - runSignOutputRaw(ht, ht.Alice) + alice := ht.Alice + + runSignOutputRaw(ht, alice) } // runSignOutputRaw makes sure that the SignOutputRaw RPC can be used with all @@ -377,7 +381,9 @@ func assertSignOutputRaw(ht *lntest.HarnessTest, // all custom flags by verifying with VerifyMessage. Tests both ECDSA and // Schnorr signatures. func testSignVerifyMessage(ht *lntest.HarnessTest) { - runSignVerifyMessage(ht, ht.Alice) + alice := ht.Alice + + runSignVerifyMessage(ht, alice) } // runSignVerifyMessage makes sure that the SignMessage RPC can be used with diff --git a/itest/lnd_sweep_test.go b/itest/lnd_sweep_test.go index 5cad6269ae..025a6fee4b 100644 --- a/itest/lnd_sweep_test.go +++ b/itest/lnd_sweep_test.go @@ -1548,7 +1548,9 @@ func testSweepCommitOutputAndAnchor(ht *lntest.HarnessTest) { // CPFP, then RBF. Along the way, we check the `BumpFee` can properly update // the fee function used by supplying new params. func testBumpFee(ht *lntest.HarnessTest) { - runBumpFee(ht, ht.Alice) + alice := ht.Alice + + runBumpFee(ht, alice) } // runBumpFee checks the `BumpFee` RPC can properly bump the fee of a given diff --git a/itest/lnd_taproot_test.go b/itest/lnd_taproot_test.go index b30f8cdaab..d2dd35b528 100644 --- a/itest/lnd_taproot_test.go +++ b/itest/lnd_taproot_test.go @@ -49,34 +49,36 @@ var ( // testTaproot ensures that the daemon can send to and spend from taproot (p2tr) // outputs. func testTaproot(ht *lntest.HarnessTest) { - testTaprootSendCoinsKeySpendBip86(ht, ht.Alice) - testTaprootComputeInputScriptKeySpendBip86(ht, ht.Alice) - testTaprootSignOutputRawScriptSpend(ht, ht.Alice) + alice := ht.Alice + + testTaprootSendCoinsKeySpendBip86(ht, alice) + testTaprootComputeInputScriptKeySpendBip86(ht, alice) + testTaprootSignOutputRawScriptSpend(ht, alice) testTaprootSignOutputRawScriptSpend( - ht, ht.Alice, txscript.SigHashSingle, + ht, alice, txscript.SigHashSingle, ) - testTaprootSignOutputRawKeySpendBip86(ht, ht.Alice) + testTaprootSignOutputRawKeySpendBip86(ht, alice) testTaprootSignOutputRawKeySpendBip86( - ht, ht.Alice, txscript.SigHashSingle, + ht, alice, txscript.SigHashSingle, ) - testTaprootSignOutputRawKeySpendRootHash(ht, ht.Alice) + testTaprootSignOutputRawKeySpendRootHash(ht, alice) muSig2Versions := []signrpc.MuSig2Version{ signrpc.MuSig2Version_MUSIG2_VERSION_V040, signrpc.MuSig2Version_MUSIG2_VERSION_V100RC2, } for _, version := range muSig2Versions { - testTaprootMuSig2KeySpendBip86(ht, ht.Alice, version) - testTaprootMuSig2KeySpendRootHash(ht, ht.Alice, version) - testTaprootMuSig2ScriptSpend(ht, ht.Alice, version) - testTaprootMuSig2CombinedLeafKeySpend(ht, ht.Alice, version) - testMuSig2CombineKey(ht, ht.Alice, version) + testTaprootMuSig2KeySpendBip86(ht, alice, version) + testTaprootMuSig2KeySpendRootHash(ht, alice, version) + testTaprootMuSig2ScriptSpend(ht, alice, version) + testTaprootMuSig2CombinedLeafKeySpend(ht, alice, version) + testMuSig2CombineKey(ht, alice, version) } - testTaprootImportTapscriptFullTree(ht, ht.Alice) - testTaprootImportTapscriptPartialReveal(ht, ht.Alice) - testTaprootImportTapscriptRootHashOnly(ht, ht.Alice) - testTaprootImportTapscriptFullKey(ht, ht.Alice) + testTaprootImportTapscriptFullTree(ht, alice) + testTaprootImportTapscriptPartialReveal(ht, alice) + testTaprootImportTapscriptRootHashOnly(ht, alice) + testTaprootImportTapscriptFullKey(ht, alice) } // testTaprootSendCoinsKeySpendBip86 tests sending to and spending from diff --git a/itest/lnd_watchtower_test.go b/itest/lnd_watchtower_test.go index a68df852bc..272d19d19d 100644 --- a/itest/lnd_watchtower_test.go +++ b/itest/lnd_watchtower_test.go @@ -39,6 +39,8 @@ var watchtowerTestCases = []*lntest.TestCase{ // testTowerClientTowerAndSessionManagement tests the various control commands // that a user has over the client's set of active towers and sessions. func testTowerClientTowerAndSessionManagement(ht *lntest.HarnessTest) { + alice := ht.Alice + const ( chanAmt = funding.MaxBtcFundingAmount externalIP = "1.2.3.4" @@ -104,13 +106,13 @@ func testTowerClientTowerAndSessionManagement(ht *lntest.HarnessTest) { ht.FundCoins(btcutil.SatoshiPerBitcoin, dave) // Connect Dave and Alice. - ht.ConnectNodes(dave, ht.Alice) + ht.ConnectNodes(dave, alice) // Open a channel between Dave and Alice. params := lntest.OpenChannelParams{ Amt: chanAmt, } - chanPoint := ht.OpenChannel(dave, ht.Alice, params) + chanPoint := ht.OpenChannel(dave, alice, params) // Show that the Wallis tower is currently seen as an active session // candidate. @@ -122,7 +124,7 @@ func testTowerClientTowerAndSessionManagement(ht *lntest.HarnessTest) { // Make some back-ups and assert that they are added to a session with // the tower. - generateBackups(ht, dave, ht.Alice, 4) + generateBackups(ht, dave, alice, 4) // Assert that one of the sessions now has 4 backups. assertNumBackups(ht, dave.RPC, wallisPk, 4, false) @@ -139,7 +141,7 @@ func testTowerClientTowerAndSessionManagement(ht *lntest.HarnessTest) { require.False(ht, info.SessionInfo[0].ActiveSessionCandidate) // Back up a few more states. - generateBackups(ht, dave, ht.Alice, 4) + generateBackups(ht, dave, alice, 4) // These should _not_ be on the tower. Therefore, the number of // back-ups on the tower should be the same as before. @@ -163,7 +165,7 @@ func testTowerClientTowerAndSessionManagement(ht *lntest.HarnessTest) { }) // Generate some more back-ups. - generateBackups(ht, dave, ht.Alice, 4) + generateBackups(ht, dave, alice, 4) // Assert that they get added to the first tower (Wallis) and that the // number of sessions with Wallis has not changed - in other words, the @@ -205,7 +207,7 @@ func testTowerClientTowerAndSessionManagement(ht *lntest.HarnessTest) { assertNumSessions(wallisPk, 4, false) // Any new back-ups should now be backed up on a different session. - generateBackups(ht, dave, ht.Alice, 2) + generateBackups(ht, dave, alice, 2) assertNumBackups(ht, dave.RPC, wallisPk, 10, false) findSession(wallisPk, 2) @@ -238,6 +240,8 @@ func testTowerClientTowerAndSessionManagement(ht *lntest.HarnessTest) { // testTowerClientSessionDeletion tests that sessions are correctly deleted // when they are deemed closable. func testTowerClientSessionDeletion(ht *lntest.HarnessTest) { + alice := ht.Alice + const ( chanAmt = funding.MaxBtcFundingAmount numInvoices = 5 @@ -290,18 +294,18 @@ func testTowerClientSessionDeletion(ht *lntest.HarnessTest) { ht.FundCoins(btcutil.SatoshiPerBitcoin, dave) // Connect Dave and Alice. - ht.ConnectNodes(dave, ht.Alice) + ht.ConnectNodes(dave, alice) // Open a channel between Dave and Alice. params := lntest.OpenChannelParams{ Amt: chanAmt, } - chanPoint := ht.OpenChannel(dave, ht.Alice, params) + chanPoint := ht.OpenChannel(dave, alice, params) // Since there are 2 updates made for every payment and the maximum // number of updates per session has been set to 10, make 5 payments // between the pair so that the session is exhausted. - generateBackups(ht, dave, ht.Alice, maxUpdates) + generateBackups(ht, dave, alice, maxUpdates) // Assert that one of the sessions now has 10 backups. assertNumBackups(ht, dave.RPC, wallisPk, 10, false) From f33e55fcd3e852408727d4df136ec68bad37969b Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Wed, 20 Nov 2024 14:46:59 +0800 Subject: [PATCH 097/153] itest: remove the use of standby nodes This commit removes the usage of the standby nodes and uses `CreateSimpleNetwork` when applicable. Also introduces a helper method `NewNodeWithCoins` to quickly start a node with funds. --- itest/lnd_channel_backup_test.go | 4 +- itest/lnd_channel_balance_test.go | 5 +- itest/lnd_channel_force_close_test.go | 2 +- itest/lnd_channel_graph_test.go | 21 ++--- itest/lnd_channel_policy_test.go | 28 +++---- .../lnd_coop_close_external_delivery_test.go | 3 +- itest/lnd_coop_close_with_htlcs_test.go | 6 +- itest/lnd_custom_features.go | 13 ++- itest/lnd_custom_message_test.go | 5 +- itest/lnd_forward_interceptor_test.go | 4 +- itest/lnd_funding_test.go | 11 ++- itest/lnd_hold_invoice_force_test.go | 7 +- itest/lnd_hold_persistence_test.go | 4 +- itest/lnd_htlc_test.go | 2 +- itest/lnd_invoice_acceptor_test.go | 3 +- itest/lnd_macaroons_test.go | 6 +- itest/lnd_max_htlcs_test.go | 22 +++-- itest/lnd_misc_test.go | 59 +++++++------- itest/lnd_mpp_test.go | 4 +- itest/lnd_multi-hop-error-propagation_test.go | 4 +- itest/lnd_multi-hop-payments_test.go | 4 +- itest/lnd_network_test.go | 4 +- itest/lnd_neutrino_test.go | 2 +- itest/lnd_onchain_test.go | 15 ++-- itest/lnd_open_channel_test.go | 28 ++++--- itest/lnd_payment_test.go | 16 +++- itest/lnd_psbt_test.go | 17 ++-- itest/lnd_res_handoff_test.go | 3 +- itest/lnd_rest_api_test.go | 20 ++--- itest/lnd_route_blinding_test.go | 80 +++++++++++-------- itest/lnd_routing_test.go | 39 ++++++--- itest/lnd_signer_test.go | 6 +- itest/lnd_single_hop_invoice_test.go | 7 +- itest/lnd_sweep_test.go | 2 +- itest/lnd_switch_test.go | 4 +- itest/lnd_taproot_test.go | 2 +- itest/lnd_trackpayments_test.go | 25 +++--- itest/lnd_wallet_import_test.go | 4 +- itest/lnd_watchtower_test.go | 10 +-- itest/lnd_wipe_fwdpkgs_test.go | 23 ++---- itest/lnd_zero_conf_test.go | 2 +- lntest/harness.go | 36 +++++++++ 42 files changed, 314 insertions(+), 248 deletions(-) diff --git a/itest/lnd_channel_backup_test.go b/itest/lnd_channel_backup_test.go index d9886ed23e..d96a44d0a4 100644 --- a/itest/lnd_channel_backup_test.go +++ b/itest/lnd_channel_backup_test.go @@ -844,7 +844,7 @@ func runChanRestoreScenarioForceClose(ht *lntest.HarnessTest, zeroConf bool) { // and the on-disk channel.backup are updated each time a channel is // opened/closed. func testChannelBackupUpdates(ht *lntest.HarnessTest) { - alice := ht.Alice + alice := ht.NewNodeWithCoins("Alice", nil) // First, we'll make a temp directory that we'll use to store our // backup file, so we can check in on it during the test easily. @@ -1052,7 +1052,7 @@ func testExportChannelBackup(ht *lntest.HarnessTest) { // With Carol up, we'll now connect her to Alice, and open a channel // between them. - alice := ht.Alice + alice := ht.NewNodeWithCoins("Alice", nil) ht.ConnectNodes(carol, alice) // Next, we'll open two channels between Alice and Carol back to back. diff --git a/itest/lnd_channel_balance_test.go b/itest/lnd_channel_balance_test.go index 8ab276d2e6..82382e9110 100644 --- a/itest/lnd_channel_balance_test.go +++ b/itest/lnd_channel_balance_test.go @@ -48,7 +48,8 @@ func testChannelBalance(ht *lntest.HarnessTest) { } // Before beginning, make sure alice and bob are connected. - alice, bob := ht.Alice, ht.Bob + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNode("Bob", nil) ht.EnsureConnected(alice, bob) chanPoint := ht.OpenChannel( @@ -118,7 +119,7 @@ func testChannelUnsettledBalance(ht *lntest.HarnessTest) { carol := ht.NewNode("Carol", []string{"--hodl.exit-settle"}) // Connect Alice to Carol. - alice := ht.Alice + alice := ht.NewNodeWithCoins("Alice", nil) ht.ConnectNodes(alice, carol) // Open a channel between Alice and Carol. diff --git a/itest/lnd_channel_force_close_test.go b/itest/lnd_channel_force_close_test.go index 0ef144b1e6..fca3838898 100644 --- a/itest/lnd_channel_force_close_test.go +++ b/itest/lnd_channel_force_close_test.go @@ -769,7 +769,7 @@ func testFailingChannel(ht *lntest.HarnessTest) { // totally unrelated preimage. carol := ht.NewNode("Carol", []string{"--hodl.bogus-settle"}) - alice := ht.Alice + alice := ht.NewNodeWithCoins("Alice", nil) ht.ConnectNodes(alice, carol) // Let Alice connect and open a channel to Carol, diff --git a/itest/lnd_channel_graph_test.go b/itest/lnd_channel_graph_test.go index 54ddd2ca21..0b27286459 100644 --- a/itest/lnd_channel_graph_test.go +++ b/itest/lnd_channel_graph_test.go @@ -218,7 +218,9 @@ func testUpdateChanStatus(ht *lntest.HarnessTest) { // describeGraph RPC request unless explicitly asked for. func testUnannouncedChannels(ht *lntest.HarnessTest) { amount := funding.MaxBtcFundingAmount - alice, bob := ht.Alice, ht.Bob + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNode("Bob", nil) + ht.EnsureConnected(alice, bob) // Open a channel between Alice and Bob, ensuring the // channel has been opened properly. @@ -267,14 +269,10 @@ func testGraphTopologyNtfns(ht *lntest.HarnessTest, pinned bool) { // Spin up Bob first, since we will need to grab his pubkey when // starting Alice to test pinned syncing. - bob := ht.Bob + bob := ht.NewNodeWithCoins("Bob", nil) bobInfo := bob.RPC.GetInfo() bobPubkey := bobInfo.IdentityPubkey - // Restart Bob as he may have leftover announcements from previous - // tests, causing the graph to be unsynced. - ht.RestartNodeWithExtraArgs(bob, nil) - // For unpinned syncing, start Alice as usual. Otherwise grab Bob's // pubkey to include in his pinned syncer set. var aliceArgs []string @@ -285,8 +283,7 @@ func testGraphTopologyNtfns(ht *lntest.HarnessTest, pinned bool) { } } - alice := ht.Alice - ht.RestartNodeWithExtraArgs(alice, aliceArgs) + alice := ht.NewNodeWithCoins("Alice", aliceArgs) // Connect Alice and Bob. ht.EnsureConnected(alice, bob) @@ -379,7 +376,9 @@ func testGraphTopologyNtfns(ht *lntest.HarnessTest, pinned bool) { // external IP addresses specified on the command line, that those addresses // announced to the network and reported in the network graph. func testNodeAnnouncement(ht *lntest.HarnessTest) { - alice, bob := ht.Alice, ht.Bob + alice := ht.NewNode("Alice", nil) + bob := ht.NewNodeWithCoins("Bob", nil) + ht.EnsureConnected(alice, bob) advertisedAddrs := []string{ "192.168.1.1:8333", @@ -434,7 +433,9 @@ func testNodeAnnouncement(ht *lntest.HarnessTest) { // the requests correctly and that the new node announcement is brodcasted // with the right information after updating our node. func testUpdateNodeAnnouncement(ht *lntest.HarnessTest) { - alice, bob := ht.Alice, ht.Bob + alice := ht.NewNode("Alice", nil) + bob := ht.NewNodeWithCoins("Bob", nil) + ht.EnsureConnected(alice, bob) var lndArgs []string diff --git a/itest/lnd_channel_policy_test.go b/itest/lnd_channel_policy_test.go index bb05209753..9c188fd95c 100644 --- a/itest/lnd_channel_policy_test.go +++ b/itest/lnd_channel_policy_test.go @@ -30,19 +30,16 @@ func testUpdateChannelPolicy(ht *lntest.HarnessTest) { chanAmt := funding.MaxBtcFundingAmount pushAmt := chanAmt / 2 - alice, bob := ht.Alice, ht.Bob - // Create a channel Alice->Bob. - chanPoint := ht.OpenChannel( - alice, bob, lntest.OpenChannelParams{ + chanPoints, nodes := ht.CreateSimpleNetwork( + [][]string{nil, nil}, lntest.OpenChannelParams{ Amt: chanAmt, PushAmt: pushAmt, }, ) - // We add all the nodes' update channels to a slice, such that we can - // make sure they all receive the expected updates. - nodes := []*node.HarnessNode{alice, bob} + alice, bob := nodes[0], nodes[1] + chanPoint := chanPoints[0] // Alice and Bob should see each other's ChannelUpdates, advertising the // default routing policies. We do not currently set any inbound fees. @@ -440,7 +437,8 @@ func testUpdateChannelPolicy(ht *lntest.HarnessTest) { func testSendUpdateDisableChannel(ht *lntest.HarnessTest) { const chanAmt = 100000 - alice, bob := ht.Alice, ht.Bob + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNode("Bob", nil) // Create a new node Eve, which will be restarted later with a config // that has an inactive channel timeout of just 6 seconds (down from @@ -677,7 +675,9 @@ func testUpdateChannelPolicyForPrivateChannel(ht *lntest.HarnessTest) { // We'll create the following topology first, // Alice <--public:100k--> Bob <--private:100k--> Carol - alice, bob := ht.Alice, ht.Bob + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNodeWithCoins("Bob", nil) + ht.EnsureConnected(alice, bob) // Open a channel with 100k satoshis between Alice and Bob. chanPointAliceBob := ht.OpenChannel( @@ -786,16 +786,14 @@ func testUpdateChannelPolicyFeeRateAccuracy(ht *lntest.HarnessTest) { pushAmt := chanAmt / 2 // Create a channel Alice -> Bob. - alice, bob := ht.Alice, ht.Bob - chanPoint := ht.OpenChannel( - alice, bob, lntest.OpenChannelParams{ + chanPoints, nodes := ht.CreateSimpleNetwork( + [][]string{nil, nil}, lntest.OpenChannelParams{ Amt: chanAmt, PushAmt: pushAmt, }, ) - - // Nodes that we need to make sure receive the channel updates. - nodes := []*node.HarnessNode{alice, bob} + alice := nodes[0] + chanPoint := chanPoints[0] baseFee := int64(1500) timeLockDelta := uint32(66) diff --git a/itest/lnd_coop_close_external_delivery_test.go b/itest/lnd_coop_close_external_delivery_test.go index 57c2da2f4f..8341a2f650 100644 --- a/itest/lnd_coop_close_external_delivery_test.go +++ b/itest/lnd_coop_close_external_delivery_test.go @@ -58,7 +58,8 @@ func testCoopCloseWithExternalDelivery(ht *lntest.HarnessTest) { func testCoopCloseWithExternalDeliveryImpl(ht *lntest.HarnessTest, upfrontShutdown bool, deliveryAddressType lnrpc.AddressType) { - alice, bob := ht.Alice, ht.Bob + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNodeWithCoins("bob", nil) ht.ConnectNodes(alice, bob) // Here we generate a final delivery address in bob's wallet but set by diff --git a/itest/lnd_coop_close_with_htlcs_test.go b/itest/lnd_coop_close_with_htlcs_test.go index 2b7bcad729..b5d5f516b9 100644 --- a/itest/lnd_coop_close_with_htlcs_test.go +++ b/itest/lnd_coop_close_with_htlcs_test.go @@ -37,7 +37,8 @@ func testCoopCloseWithHtlcs(ht *lntest.HarnessTest) { // channel party initiates a channel shutdown while an HTLC is still pending on // the channel. func coopCloseWithHTLCs(ht *lntest.HarnessTest) { - alice, bob := ht.Alice, ht.Bob + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNodeWithCoins("bob", nil) ht.ConnectNodes(alice, bob) // Here we set up a channel between Alice and Bob, beginning with a @@ -128,7 +129,8 @@ func coopCloseWithHTLCs(ht *lntest.HarnessTest) { // process continues as expected even if a channel re-establish happens after // one party has already initiated the shutdown. func coopCloseWithHTLCsWithRestart(ht *lntest.HarnessTest) { - alice, bob := ht.Alice, ht.Bob + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNodeWithCoins("bob", nil) ht.ConnectNodes(alice, bob) // Open a channel between Alice and Bob with the balance split equally. diff --git a/itest/lnd_custom_features.go b/itest/lnd_custom_features.go index c7cec8f1dd..cba6ee4aaf 100644 --- a/itest/lnd_custom_features.go +++ b/itest/lnd_custom_features.go @@ -13,8 +13,6 @@ import ( // sets. For completeness, it also asserts that features aren't set in places // where they aren't intended to be. func testCustomFeatures(ht *lntest.HarnessTest) { - alice, bob := ht.Alice, ht.Bob - var ( // Odd custom features so that we don't need to worry about // issues connecting to peers. @@ -29,14 +27,13 @@ func testCustomFeatures(ht *lntest.HarnessTest) { fmt.Sprintf("--protocol.custom-nodeann=%v", customNodeAnn), fmt.Sprintf("--protocol.custom-invoice=%v", customInvoice), } - ht.RestartNodeWithExtraArgs(alice, extraArgs) + cfgs := [][]string{extraArgs, nil} - // Connect nodes and open a channel so that Alice will be included - // in Bob's graph. - ht.ConnectNodes(alice, bob) - chanPoint := ht.OpenChannel( - alice, bob, lntest.OpenChannelParams{Amt: 1000000}, + chanPoints, nodes := ht.CreateSimpleNetwork( + cfgs, lntest.OpenChannelParams{Amt: 1000000}, ) + alice, bob := nodes[0], nodes[1] + chanPoint := chanPoints[0] // Check that Alice's custom feature bit was sent to Bob in her init // message. diff --git a/itest/lnd_custom_message_test.go b/itest/lnd_custom_message_test.go index 14a4e79493..93a92a2d66 100644 --- a/itest/lnd_custom_message_test.go +++ b/itest/lnd_custom_message_test.go @@ -14,8 +14,6 @@ import ( // types (within the message type range usually reserved for protocol messages) // via the send and subscribe custom message APIs. func testCustomMessage(ht *lntest.HarnessTest) { - alice, bob := ht.Alice, ht.Bob - var ( overrideType1 uint32 = 554 overrideType2 uint32 = 555 @@ -27,7 +25,8 @@ func testCustomMessage(ht *lntest.HarnessTest) { extraArgs := []string{ fmt.Sprintf(msgOverrideArg, overrideType1), } - ht.RestartNodeWithExtraArgs(alice, extraArgs) + alice := ht.NewNode("Alice", extraArgs) + bob := ht.NewNode("Bob", nil) // Subscribe Alice to custom messages before we send any, so that we // don't miss any. diff --git a/itest/lnd_forward_interceptor_test.go b/itest/lnd_forward_interceptor_test.go index 6d6cdef7e9..11fa01e038 100644 --- a/itest/lnd_forward_interceptor_test.go +++ b/itest/lnd_forward_interceptor_test.go @@ -762,7 +762,9 @@ type interceptorTestScenario struct { func newInterceptorTestScenario( ht *lntest.HarnessTest) *interceptorTestScenario { - alice, bob := ht.Alice, ht.Bob + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNodeWithCoins("bob", nil) + carol := ht.NewNode("carol", nil) dave := ht.NewNode("dave", nil) diff --git a/itest/lnd_funding_test.go b/itest/lnd_funding_test.go index f9888336d7..022fedeafa 100644 --- a/itest/lnd_funding_test.go +++ b/itest/lnd_funding_test.go @@ -287,8 +287,7 @@ func testUnconfirmedChannelFunding(ht *lntest.HarnessTest) { // We'll start off by creating a node for Carol. carol := ht.NewNode("Carol", nil) - - alice := ht.Alice + alice := ht.NewNode("Alice", nil) // We'll send her some unconfirmed funds. ht.FundCoinsUnconfirmed(2*chanAmt, carol) @@ -407,7 +406,7 @@ func testUnconfirmedChannelFunding(ht *lntest.HarnessTest) { // testChannelFundingInputTypes tests that any type of supported input type can // be used to fund channels. func testChannelFundingInputTypes(ht *lntest.HarnessTest) { - alice := ht.Alice + alice := ht.NewNode("Alice", nil) // We'll start off by creating a node for Carol. carol := ht.NewNode("Carol", nil) @@ -846,7 +845,7 @@ func testChannelFundingPersistence(ht *lntest.HarnessTest) { } carol := ht.NewNode("Carol", carolArgs) - alice := ht.Alice + alice := ht.NewNodeWithCoins("Alice", nil) ht.ConnectNodes(alice, carol) // Create a new channel that requires 5 confs before it's considered @@ -962,8 +961,8 @@ func testBatchChanFunding(ht *lntest.HarnessTest) { } eve := ht.NewNode("eve", scidAliasArgs) - alice, bob := ht.Alice, ht.Bob - ht.RestartNodeWithExtraArgs(alice, scidAliasArgs) + alice := ht.NewNodeWithCoins("Alice", scidAliasArgs) + bob := ht.NewNodeWithCoins("Bob", nil) // Before we start the test, we'll ensure Alice is connected to Carol // and Dave, so she can open channels to both of them (and Bob). diff --git a/itest/lnd_hold_invoice_force_test.go b/itest/lnd_hold_invoice_force_test.go index d9b8517107..5f0a54670b 100644 --- a/itest/lnd_hold_invoice_force_test.go +++ b/itest/lnd_hold_invoice_force_test.go @@ -17,10 +17,11 @@ import ( // would otherwise trigger force closes when they expire. func testHoldInvoiceForceClose(ht *lntest.HarnessTest) { // Open a channel between alice and bob. - alice, bob := ht.Alice, ht.Bob - chanPoint := ht.OpenChannel( - alice, bob, lntest.OpenChannelParams{Amt: 300000}, + chanPoints, nodes := ht.CreateSimpleNetwork( + [][]string{nil, nil}, lntest.OpenChannelParams{Amt: 300000}, ) + alice, bob := nodes[0], nodes[1] + chanPoint := chanPoints[0] // Create a non-dust hold invoice for bob. var ( diff --git a/itest/lnd_hold_persistence_test.go b/itest/lnd_hold_persistence_test.go index f1c2a7d823..03c5911371 100644 --- a/itest/lnd_hold_persistence_test.go +++ b/itest/lnd_hold_persistence_test.go @@ -28,7 +28,9 @@ func testHoldInvoicePersistence(ht *lntest.HarnessTest) { carol := ht.NewNode("Carol", nil) // Connect Alice to Carol. - alice, bob := ht.Alice, ht.Bob + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNode("Bob", nil) + ht.EnsureConnected(alice, bob) ht.ConnectNodes(alice, carol) // Open a channel between Alice and Carol which is private so that we diff --git a/itest/lnd_htlc_test.go b/itest/lnd_htlc_test.go index fc04fe8d0f..001a4dc65a 100644 --- a/itest/lnd_htlc_test.go +++ b/itest/lnd_htlc_test.go @@ -14,7 +14,7 @@ import ( func testLookupHtlcResolution(ht *lntest.HarnessTest) { const chanAmt = btcutil.Amount(1000000) - alice := ht.Alice + alice := ht.NewNodeWithCoins("Alice", nil) carol := ht.NewNode("Carol", []string{ "--store-final-htlc-resolutions", }) diff --git a/itest/lnd_invoice_acceptor_test.go b/itest/lnd_invoice_acceptor_test.go index 97d14650c1..d27d13d9db 100644 --- a/itest/lnd_invoice_acceptor_test.go +++ b/itest/lnd_invoice_acceptor_test.go @@ -247,7 +247,8 @@ type acceptorTestScenario struct { // // Among them, Alice and Bob are standby nodes and Carol is a new node. func newAcceptorTestScenario(ht *lntest.HarnessTest) *acceptorTestScenario { - alice, bob := ht.Alice, ht.Bob + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNodeWithCoins("bob", nil) carol := ht.NewNode("carol", nil) ht.EnsureConnected(alice, bob) diff --git a/itest/lnd_macaroons_test.go b/itest/lnd_macaroons_test.go index 3adfafffe9..b896d455ac 100644 --- a/itest/lnd_macaroons_test.go +++ b/itest/lnd_macaroons_test.go @@ -29,7 +29,7 @@ func testMacaroonAuthentication(ht *lntest.HarnessTest) { newAddrReq = &lnrpc.NewAddressRequest{ Type: AddrTypeWitnessPubkeyHash, } - testNode = ht.Alice + testNode = ht.NewNode("Alice", nil) testClient = testNode.RPC.LN ) @@ -295,7 +295,7 @@ func testMacaroonAuthentication(ht *lntest.HarnessTest) { // in the request must be set correctly, and the baked macaroon has the intended // permissions. func testBakeMacaroon(ht *lntest.HarnessTest) { - var testNode = ht.Alice + var testNode = ht.NewNode("Alice", nil) testCases := []struct { name string @@ -521,7 +521,7 @@ func testBakeMacaroon(ht *lntest.HarnessTest) { func testDeleteMacaroonID(ht *lntest.HarnessTest) { var ( ctxb = ht.Context() - testNode = ht.Alice + testNode = ht.NewNode("Alice", nil) ) ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) defer cancel() diff --git a/itest/lnd_max_htlcs_test.go b/itest/lnd_max_htlcs_test.go index 971537adf4..a22a946a76 100644 --- a/itest/lnd_max_htlcs_test.go +++ b/itest/lnd_max_htlcs_test.go @@ -19,25 +19,21 @@ func testMaxHtlcPathfind(ht *lntest.HarnessTest) { // Bob to add a maximum of 5 htlcs to her commitment. maxHtlcs := 5 - alice, bob := ht.Alice, ht.Bob - - // Restart nodes with the new flag so they understand the new payment + // Create nodes with the new flag so they understand the new payment // status. - ht.RestartNodeWithExtraArgs(alice, []string{ - "--routerrpc.usestatusinitiated", - }) - ht.RestartNodeWithExtraArgs(bob, []string{ - "--routerrpc.usestatusinitiated", - }) - - ht.EnsureConnected(alice, bob) - chanPoint := ht.OpenChannel( - alice, bob, lntest.OpenChannelParams{ + cfg := []string{"--routerrpc.usestatusinitiated"} + cfgs := [][]string{cfg, cfg} + + // Create a channel Alice->Bob. + chanPoints, nodes := ht.CreateSimpleNetwork( + cfgs, lntest.OpenChannelParams{ Amt: 1000000, PushAmt: 800000, RemoteMaxHtlcs: uint16(maxHtlcs), }, ) + chanPoint := chanPoints[0] + alice, bob := nodes[0], nodes[1] // Alice and bob should have one channel open with each other now. ht.AssertNodeNumChannels(alice, 1) diff --git a/itest/lnd_misc_test.go b/itest/lnd_misc_test.go index 21873109ff..70fcbe971b 100644 --- a/itest/lnd_misc_test.go +++ b/itest/lnd_misc_test.go @@ -39,9 +39,8 @@ func testDisconnectingTargetPeer(ht *lntest.HarnessTest) { "--maxbackoff=1m", } - alice, bob := ht.Alice, ht.Bob - ht.RestartNodeWithExtraArgs(alice, args) - ht.RestartNodeWithExtraArgs(bob, args) + alice := ht.NewNodeWithCoins("Alice", args) + bob := ht.NewNodeWithCoins("Bob", args) // Start by connecting Alice and Bob with no channels. ht.EnsureConnected(alice, bob) @@ -239,17 +238,11 @@ func testListChannels(ht *lntest.HarnessTest) { const aliceRemoteMaxHtlcs = 50 const bobRemoteMaxHtlcs = 100 - // Get the standby nodes and open a channel between them. - alice, bob := ht.Alice, ht.Bob - args := []string{fmt.Sprintf( "--default-remote-max-htlcs=%v", bobRemoteMaxHtlcs, )} - ht.RestartNodeWithExtraArgs(bob, args) - - // Connect Alice to Bob. - ht.EnsureConnected(alice, bob) + cfgs := [][]string{nil, args} // Open a channel with 100k satoshis between Alice and Bob with Alice // being the sole funder of the channel. The minial HTLC amount is set @@ -264,8 +257,10 @@ func testListChannels(ht *lntest.HarnessTest) { MinHtlc: customizedMinHtlc, RemoteMaxHtlcs: aliceRemoteMaxHtlcs, } - chanPoint := ht.OpenChannel(alice, bob, p) - defer ht.CloseChannel(alice, chanPoint) + + chanPoints, nodes := ht.CreateSimpleNetwork(cfgs, p) + alice, bob := nodes[0], nodes[1] + chanPoint := chanPoints[0] // Alice should have one channel opened with Bob. ht.AssertNodeNumChannels(alice, 1) @@ -369,7 +364,7 @@ func testMaxPendingChannels(ht *lntest.HarnessTest) { } carol := ht.NewNode("Carol", args) - alice := ht.Alice + alice := ht.NewNodeWithCoins("Alice", nil) ht.ConnectNodes(alice, carol) carolBalance := btcutil.Amount(maxPendingChannels) * amount @@ -439,7 +434,9 @@ func testMaxPendingChannels(ht *lntest.HarnessTest) { func testGarbageCollectLinkNodes(ht *lntest.HarnessTest) { const chanAmt = 1000000 - alice, bob := ht.Alice, ht.Bob + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNodeWithCoins("Bob", nil) + ht.EnsureConnected(alice, bob) // Open a channel between Alice and Bob which will later be // cooperatively closed. @@ -553,7 +550,8 @@ func testRejectHTLC(ht *lntest.HarnessTest) { // Alice ------> Carol ------> Bob // const chanAmt = btcutil.Amount(1000000) - alice, bob := ht.Alice, ht.Bob + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNodeWithCoins("Bob", nil) // Create Carol with reject htlc flag. carol := ht.NewNode("Carol", []string{"--rejecthtlc"}) @@ -649,15 +647,16 @@ func testRejectHTLC(ht *lntest.HarnessTest) { func testNodeSignVerify(ht *lntest.HarnessTest) { chanAmt := funding.MaxBtcFundingAmount pushAmt := btcutil.Amount(100000) - alice, bob := ht.Alice, ht.Bob + p := lntest.OpenChannelParams{ + Amt: chanAmt, + PushAmt: pushAmt, + } // Create a channel between alice and bob. - aliceBobCh := ht.OpenChannel( - alice, bob, lntest.OpenChannelParams{ - Amt: chanAmt, - PushAmt: pushAmt, - }, - ) + cfgs := [][]string{nil, nil} + chanPoints, nodes := ht.CreateSimpleNetwork(cfgs, p) + alice, bob := nodes[0], nodes[1] + aliceBobCh := chanPoints[0] // alice signs "alice msg" and sends her signature to bob. aliceMsg := []byte("alice msg") @@ -694,14 +693,17 @@ func testNodeSignVerify(ht *lntest.HarnessTest) { // and not in one of the pending closure states. It also verifies that the // abandoned channel is reported as closed with close type 'abandoned'. func testAbandonChannel(ht *lntest.HarnessTest) { - alice, bob := ht.Alice, ht.Bob - // First establish a channel between Alice and Bob. channelParam := lntest.OpenChannelParams{ Amt: funding.MaxBtcFundingAmount, PushAmt: btcutil.Amount(100000), } - chanPoint := ht.OpenChannel(alice, bob, channelParam) + + // Create a channel between alice and bob. + cfgs := [][]string{nil, nil} + chanPoints, nodes := ht.CreateSimpleNetwork(cfgs, channelParam) + alice, bob := nodes[0], nodes[1] + chanPoint := chanPoints[0] // Now that the channel is open, we'll obtain its channel ID real quick // so we can use it to query the graph below. @@ -763,7 +765,7 @@ func testAbandonChannel(ht *lntest.HarnessTest) { // testSendAllCoins tests that we're able to properly sweep all coins from the // wallet into a single target address at the specified fee rate. func testSendAllCoins(ht *lntest.HarnessTest) { - alice := ht.Alice + alice := ht.NewNodeWithCoins("Alice", nil) // First, we'll make a new node, Ainz who'll we'll use to test wallet // sweeping. @@ -1162,7 +1164,8 @@ func assertChannelConstraintsEqual(ht *lntest.HarnessTest, // on a message with a provided address. func testSignVerifyMessageWithAddr(ht *lntest.HarnessTest) { // Using different nodes to sign the message and verify the signature. - alice, bob := ht.Alice, ht.Bob + alice := ht.NewNode("Alice,", nil) + bob := ht.NewNode("Bob,", nil) // Test an lnd wallet created P2WKH address. respAddr := alice.RPC.NewAddress(&lnrpc.NewAddressRequest{ @@ -1279,7 +1282,7 @@ func testSignVerifyMessageWithAddr(ht *lntest.HarnessTest) { // up with native SQL enabled, as we don't currently support migration of KV // invoices to the new SQL schema. func testNativeSQLNoMigration(ht *lntest.HarnessTest) { - alice := ht.Alice + alice := ht.NewNode("Alice", nil) // Make sure we run the test with SQLite or Postgres. if alice.Cfg.DBBackend != node.BackendSqlite && diff --git a/itest/lnd_mpp_test.go b/itest/lnd_mpp_test.go index 800708569d..99eb1eb599 100644 --- a/itest/lnd_mpp_test.go +++ b/itest/lnd_mpp_test.go @@ -180,8 +180,8 @@ type mppTestScenario struct { // \ / // \__ Dave ____/ func newMppTestScenario(ht *lntest.HarnessTest) *mppTestScenario { - alice, bob := ht.Alice, ht.Bob - ht.RestartNodeWithExtraArgs(bob, []string{ + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNodeWithCoins("Bob", []string{ "--maxpendingchannels=2", "--accept-amp", }) diff --git a/itest/lnd_multi-hop-error-propagation_test.go b/itest/lnd_multi-hop-error-propagation_test.go index 6fe34fdcfc..12ec3430fb 100644 --- a/itest/lnd_multi-hop-error-propagation_test.go +++ b/itest/lnd_multi-hop-error-propagation_test.go @@ -15,7 +15,9 @@ func testHtlcErrorPropagation(ht *lntest.HarnessTest) { // multi-hop payment. const chanAmt = funding.MaxBtcFundingAmount - alice, bob := ht.Alice, ht.Bob + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNodeWithCoins("Bob", nil) + ht.EnsureConnected(alice, bob) // Since we'd like to test some multi-hop failure scenarios, we'll // introduce another node into our test network: Carol. diff --git a/itest/lnd_multi-hop-payments_test.go b/itest/lnd_multi-hop-payments_test.go index ba7797d7fc..84c9800aba 100644 --- a/itest/lnd_multi-hop-payments_test.go +++ b/itest/lnd_multi-hop-payments_test.go @@ -18,7 +18,8 @@ func testMultiHopPayments(ht *lntest.HarnessTest) { // channel with Alice, and Carol with Dave. After this setup, the // network topology should now look like: // Carol -> Dave -> Alice -> Bob - alice, bob := ht.Alice, ht.Bob + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNode("Bob", nil) daveArgs := []string{"--protocol.legacy.onion"} dave := ht.NewNode("Dave", daveArgs) @@ -37,6 +38,7 @@ func testMultiHopPayments(ht *lntest.HarnessTest) { ht.AssertHtlcEventType(daveEvents, routerrpc.HtlcEvent_UNKNOWN) // Connect the nodes. + ht.ConnectNodes(alice, bob) ht.ConnectNodes(dave, alice) ht.ConnectNodes(carol, dave) diff --git a/itest/lnd_network_test.go b/itest/lnd_network_test.go index fd17d04657..ff305b59f4 100644 --- a/itest/lnd_network_test.go +++ b/itest/lnd_network_test.go @@ -126,7 +126,7 @@ func testReconnectAfterIPChange(ht *lntest.HarnessTest) { } // Connect Alice to Dave and Charlie. - alice := ht.Alice + alice := ht.NewNodeWithCoins("Alice", nil) ht.ConnectNodes(alice, dave) ht.ConnectNodes(alice, charlie) @@ -218,7 +218,7 @@ func testReconnectAfterIPChange(ht *lntest.HarnessTest) { // testAddPeerConfig tests that the "--addpeer" config flag successfully adds // a new peer. func testAddPeerConfig(ht *lntest.HarnessTest) { - alice := ht.Alice + alice := ht.NewNode("Alice", nil) info := alice.RPC.GetInfo() alicePeerAddress := info.Uris[0] diff --git a/itest/lnd_neutrino_test.go b/itest/lnd_neutrino_test.go index 39531bb84c..22f19d848e 100644 --- a/itest/lnd_neutrino_test.go +++ b/itest/lnd_neutrino_test.go @@ -13,7 +13,7 @@ func testNeutrino(ht *lntest.HarnessTest) { ht.Skipf("skipping test for non neutrino backends") } - alice := ht.Alice + alice := ht.NewNode("Alice", nil) // Check if the neutrino sub server is running. statusRes := alice.RPC.Status(nil) diff --git a/itest/lnd_onchain_test.go b/itest/lnd_onchain_test.go index 704676d56b..8b14092558 100644 --- a/itest/lnd_onchain_test.go +++ b/itest/lnd_onchain_test.go @@ -33,7 +33,7 @@ func testChainKit(ht *lntest.HarnessTest) { // testChainKitGetBlock ensures that given a block hash, the RPC endpoint // returns the correct target block. func testChainKitGetBlock(ht *lntest.HarnessTest) { - alice := ht.Alice + alice := ht.NewNode("Alice", nil) // Get best block hash. bestBlockRes := alice.RPC.GetBestBlock(nil) @@ -63,7 +63,7 @@ func testChainKitGetBlock(ht *lntest.HarnessTest) { // testChainKitGetBlockHeader ensures that given a block hash, the RPC endpoint // returns the correct target block header. func testChainKitGetBlockHeader(ht *lntest.HarnessTest) { - alice := ht.Alice + alice := ht.NewNode("Alice", nil) // Get best block hash. bestBlockRes := alice.RPC.GetBestBlock(nil) @@ -108,7 +108,7 @@ func testChainKitGetBlockHeader(ht *lntest.HarnessTest) { // testChainKitGetBlockHash ensures that given a block height, the RPC endpoint // returns the correct target block hash. func testChainKitGetBlockHash(ht *lntest.HarnessTest) { - alice := ht.Alice + alice := ht.NewNode("Alice", nil) // Get best block hash. bestBlockRes := alice.RPC.GetBestBlock(nil) @@ -134,8 +134,7 @@ func testChainKitSendOutputsAnchorReserve(ht *lntest.HarnessTest) { // NOTE: we cannot reuse the standby node here as the test requires the // node to start with no UTXOs. charlie := ht.NewNode("Charlie", args) - bob := ht.Bob - ht.RestartNodeWithExtraArgs(bob, args) + bob := ht.NewNode("Bob", args) // We'll start the test by sending Charlie some coins. fundingAmount := btcutil.Amount(100_000) @@ -222,12 +221,8 @@ func testAnchorReservedValue(ht *lntest.HarnessTest) { // Start two nodes supporting anchor channels. args := lntest.NodeArgsForCommitType(lnrpc.CommitmentType_ANCHORS) - // NOTE: we cannot reuse the standby node here as the test requires the - // node to start with no UTXOs. alice := ht.NewNode("Alice", args) - bob := ht.Bob - ht.RestartNodeWithExtraArgs(bob, args) - + bob := ht.NewNode("Bob", args) ht.ConnectNodes(alice, bob) // Send just enough coins for Alice to open a channel without a change diff --git a/itest/lnd_open_channel_test.go b/itest/lnd_open_channel_test.go index b47a766d53..20f456ea9b 100644 --- a/itest/lnd_open_channel_test.go +++ b/itest/lnd_open_channel_test.go @@ -34,7 +34,9 @@ func testOpenChannelAfterReorg(ht *lntest.HarnessTest) { tempMiner := ht.SpawnTempMiner() miner := ht.Miner() - alice, bob := ht.Alice, ht.Bob + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNode("Bob", nil) + ht.EnsureConnected(alice, bob) // Create a new channel that requires 1 confs before it's considered // open, then broadcast the funding transaction @@ -242,9 +244,10 @@ func testOpenChannelUpdateFeePolicy(ht *lntest.HarnessTest) { // In this basic test, we'll need a third node, Carol, so we can forward // a payment through the channel we'll open with the different fee // policies. + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNode("Bob", nil) carol := ht.NewNode("Carol", nil) - alice, bob := ht.Alice, ht.Bob nodes := []*node.HarnessNode{alice, bob, carol} runTestCase := func(ht *lntest.HarnessTest, @@ -301,7 +304,7 @@ func testOpenChannelUpdateFeePolicy(ht *lntest.HarnessTest) { // Send Carol enough coins to be able to open a channel // to Alice. - ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) + st.FundCoins(btcutil.SatoshiPerBitcoin, carol) runTestCase( st, feeScenario, @@ -315,7 +318,9 @@ func testOpenChannelUpdateFeePolicy(ht *lntest.HarnessTest) { // closing, and ensures that if a node is subscribed to channel updates they // will be received correctly for both cooperative and force closed channels. func testBasicChannelCreationAndUpdates(ht *lntest.HarnessTest) { - alice, bob := ht.Alice, ht.Bob + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNodeWithCoins("Bob", nil) + ht.EnsureConnected(alice, bob) runBasicChannelCreationAndUpdates(ht, alice, bob) } @@ -519,8 +524,8 @@ func testUpdateOnPendingOpenChannels(ht *lntest.HarnessTest) { // processing the fundee's `channel_ready`, the HTLC will be cached and // eventually settled. func testUpdateOnFunderPendingOpenChannels(ht *lntest.HarnessTest) { - // Grab the channel participants. - alice, bob := ht.Alice, ht.Bob + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNode("Bob", nil) // Restart Alice with the config so she won't process Bob's // channel_ready msg immediately. @@ -603,8 +608,8 @@ func testUpdateOnFunderPendingOpenChannels(ht *lntest.HarnessTest) { // processing the funder's `channel_ready`, the HTLC will be cached and // eventually settled. func testUpdateOnFundeePendingOpenChannels(ht *lntest.HarnessTest) { - // Grab the channel participants. - alice, bob := ht.Alice, ht.Bob + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNode("Bob", nil) // Restart Bob with the config so he won't process Alice's // channel_ready msg immediately. @@ -746,7 +751,10 @@ func verifyCloseUpdate(chanUpdate *lnrpc.ChannelEventUpdate, // before the funding transaction is confirmed, that the FundingExpiryBlocks // field of a PendingChannels decreases. func testFundingExpiryBlocksOnPending(ht *lntest.HarnessTest) { - alice, bob := ht.Alice, ht.Bob + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNode("Bob", nil) + ht.EnsureConnected(alice, bob) + param := lntest.OpenChannelParams{Amt: 100000} update := ht.OpenChannelAssertPending(alice, bob, param) @@ -844,7 +852,6 @@ func testSimpleTaprootChannelActivation(ht *lntest.HarnessTest) { // up as locked balance in the WalletBalance response. func testOpenChannelLockedBalance(ht *lntest.HarnessTest) { var ( - bob = ht.Bob req *lnrpc.ChannelAcceptRequest err error ) @@ -852,6 +859,7 @@ func testOpenChannelLockedBalance(ht *lntest.HarnessTest) { // Create a new node so we can assert exactly how much fund has been // locked later. alice := ht.NewNode("alice", nil) + bob := ht.NewNode("bob", nil) ht.FundCoins(btcutil.SatoshiPerBitcoin, alice) // Connect the nodes. diff --git a/itest/lnd_payment_test.go b/itest/lnd_payment_test.go index 4df148d95e..6c6fc1b178 100644 --- a/itest/lnd_payment_test.go +++ b/itest/lnd_payment_test.go @@ -346,7 +346,8 @@ func runTestPaymentHTLCTimeout(ht *lntest.HarnessTest, restartAlice bool) { // to return floor fee rate(1 sat/vb). func testSendDirectPayment(ht *lntest.HarnessTest) { // Grab Alice and Bob's nodes for convenience. - alice, bob := ht.Alice, ht.Bob + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNodeWithCoins("Bob", nil) // Create a list of commitment types we want to test. commitmentTypes := []lnrpc.CommitmentType{ @@ -466,7 +467,9 @@ func testSendDirectPayment(ht *lntest.HarnessTest) { } func testListPayments(ht *lntest.HarnessTest) { - alice, bob := ht.Alice, ht.Bob + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNode("Bob", nil) + ht.EnsureConnected(alice, bob) // Check that there are no payments before test. ht.AssertNumPayments(alice, 0) @@ -658,7 +661,10 @@ func testPaymentFollowingChannelOpen(ht *lntest.HarnessTest) { channelCapacity := paymentAmt * 1000 // We first establish a channel between Alice and Bob. - alice, bob := ht.Alice, ht.Bob + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNode("Bob", nil) + ht.EnsureConnected(alice, bob) + p := lntest.OpenChannelParams{ Amt: channelCapacity, } @@ -934,7 +940,9 @@ func testBidirectionalAsyncPayments(ht *lntest.HarnessTest) { func testInvoiceSubscriptions(ht *lntest.HarnessTest) { const chanAmt = btcutil.Amount(500000) - alice, bob := ht.Alice, ht.Bob + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNode("Bob", nil) + ht.EnsureConnected(alice, bob) // Create a new invoice subscription client for Bob, the notification // should be dispatched shortly below. diff --git a/itest/lnd_psbt_test.go b/itest/lnd_psbt_test.go index deecbdbf22..f3687d74b5 100644 --- a/itest/lnd_psbt_test.go +++ b/itest/lnd_psbt_test.go @@ -146,7 +146,7 @@ func runPsbtChanFunding(ht *lntest.HarnessTest, carol, dave *node.HarnessNode, // Before we start the test, we'll ensure both sides are connected so // the funding flow can be properly executed. - alice := ht.Alice + alice := ht.NewNodeWithCoins("Alice", nil) ht.EnsureConnected(carol, dave) ht.EnsureConnected(carol, alice) @@ -344,7 +344,7 @@ func runPsbtChanFundingExternal(ht *lntest.HarnessTest, carol, // Before we start the test, we'll ensure both sides are connected so // the funding flow can be properly executed. - alice := ht.Alice + alice := ht.NewNodeWithCoins("Alice", nil) ht.EnsureConnected(carol, dave) ht.EnsureConnected(carol, alice) @@ -517,8 +517,7 @@ func runPsbtChanFundingSingleStep(ht *lntest.HarnessTest, carol, const chanSize = funding.MaxBtcFundingAmount - alice := ht.Alice - ht.FundCoins(btcutil.SatoshiPerBitcoin, alice) + alice := ht.NewNodeWithCoins("Alice", nil) // Get new address for anchor reserve. req := &lnrpc.NewAddressRequest{ @@ -697,7 +696,8 @@ func testSignPsbt(ht *lntest.HarnessTest) { for _, tc := range psbtTestRunners { succeed := ht.Run(tc.name, func(t *testing.T) { st := ht.Subtest(t) - tc.runner(st, st.Alice) + alice := st.NewNodeWithCoins("Alice", nil) + tc.runner(st, alice) }) // Abort the test if failed. @@ -1088,7 +1088,8 @@ func runFundAndSignPsbt(ht *lntest.HarnessTest, alice *node.HarnessNode) { // a PSBT that already specifies an input but where the user still wants the // wallet to perform coin selection. func testFundPsbt(ht *lntest.HarnessTest) { - alice, bob := ht.Alice, ht.Bob + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNodeWithCoins("Bob", nil) // We test a pay-join between Alice and Bob. Bob wants to send Alice // 5 million Satoshis in a non-obvious way. So Bob selects a UTXO that's @@ -1598,8 +1599,8 @@ func sendAllCoinsToAddrType(ht *lntest.HarnessTest, // the channel opening. The psbt funding flow is used to simulate this behavior // because we can easily let the remote peer run into the timeout. func testPsbtChanFundingFailFlow(ht *lntest.HarnessTest) { - alice := ht.Alice - bob := ht.Bob + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNodeWithCoins("Bob", nil) const chanSize = funding.MaxBtcFundingAmount diff --git a/itest/lnd_res_handoff_test.go b/itest/lnd_res_handoff_test.go index dbf286293b..7238bf6b76 100644 --- a/itest/lnd_res_handoff_test.go +++ b/itest/lnd_res_handoff_test.go @@ -17,7 +17,8 @@ func testResHandoff(ht *lntest.HarnessTest) { paymentAmt = 50000 ) - alice, bob := ht.Alice, ht.Bob + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNodeWithCoins("Bob", nil) // First we'll create a channel between Alice and Bob. ht.EnsureConnected(alice, bob) diff --git a/itest/lnd_rest_api_test.go b/itest/lnd_rest_api_test.go index 42a4783c21..e559230d9a 100644 --- a/itest/lnd_rest_api_test.go +++ b/itest/lnd_rest_api_test.go @@ -212,13 +212,13 @@ func testRestAPI(ht *lntest.HarnessTest) { // Make sure Alice allows all CORS origins. Bob will keep the default. // We also make sure the ping/pong messages are sent very often, so we // can test them without waiting half a minute. - alice, bob := ht.Alice, ht.Bob - alice.Cfg.ExtraArgs = append( - alice.Cfg.ExtraArgs, "--restcors=\"*\"", + bob := ht.NewNode("Bob", nil) + args := []string{ + "--restcors=\"*\"", fmt.Sprintf("--ws-ping-interval=%s", pingInterval), fmt.Sprintf("--ws-pong-wait=%s", pongWait), - ) - ht.RestartNode(alice) + } + alice := ht.NewNodeWithCoins("Alice", args) for _, tc := range testCases { tc := tc @@ -237,7 +237,7 @@ func testRestAPI(ht *lntest.HarnessTest) { } func wsTestCaseSubscription(ht *lntest.HarnessTest) { - alice := ht.Alice + alice := ht.NewNode("Alice", nil) // Find out the current best block so we can subscribe to the next one. hash, height := ht.GetBestBlock() @@ -328,7 +328,7 @@ func wsTestCaseSubscriptionMacaroon(ht *lntest.HarnessTest) { // This time we send the macaroon in the special header // Sec-Websocket-Protocol which is the only header field available to // browsers when opening a WebSocket. - alice := ht.Alice + alice := ht.NewNode("Alice", nil) mac, err := alice.ReadMacaroon( alice.Cfg.AdminMacPath, defaultTimeout, ) @@ -413,7 +413,7 @@ func wsTestCaseBiDirectionalSubscription(ht *lntest.HarnessTest) { // This time we send the macaroon in the special header // Sec-Websocket-Protocol which is the only header field available to // browsers when opening a WebSocket. - alice := ht.Alice + alice := ht.NewNode("Alice", nil) mac, err := alice.ReadMacaroon( alice.Cfg.AdminMacPath, defaultTimeout, ) @@ -522,7 +522,7 @@ func wsTestCaseBiDirectionalSubscription(ht *lntest.HarnessTest) { // Before we start opening channels, make sure the two nodes are // connected. - bob := ht.Bob + bob := ht.NewNodeWithCoins("Bob", nil) ht.EnsureConnected(alice, bob) // Open 3 channels to make sure multiple requests and responses can be @@ -554,7 +554,7 @@ func wsTestPingPongTimeout(ht *lntest.HarnessTest) { // This time we send the macaroon in the special header // Sec-Websocket-Protocol which is the only header field available to // browsers when opening a WebSocket. - alice := ht.Alice + alice := ht.NewNode("Alice", nil) mac, err := alice.ReadMacaroon( alice.Cfg.AdminMacPath, defaultTimeout, ) diff --git a/itest/lnd_route_blinding_test.go b/itest/lnd_route_blinding_test.go index f09d5dc35d..f0ef2e4a0a 100644 --- a/itest/lnd_route_blinding_test.go +++ b/itest/lnd_route_blinding_test.go @@ -26,11 +26,9 @@ import ( // expected. It also includes the edge case of a single-hop blinded route, // which indicates that the introduction node is the recipient. func testQueryBlindedRoutes(ht *lntest.HarnessTest) { - var ( - // Convenience aliases. - alice = ht.Alice - bob = ht.Bob - ) + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNodeWithCoins("Bob", nil) + ht.EnsureConnected(alice, bob) // Setup a two hop channel network: Alice -- Bob -- Carol. // We set our proportional fee for these channels to zero, so that @@ -318,6 +316,8 @@ func testQueryBlindedRoutes(ht *lntest.HarnessTest) { type blindedForwardTest struct { ht *lntest.HarnessTest + alice *node.HarnessNode + bob *node.HarnessNode carol *node.HarnessNode dave *node.HarnessNode channels []*lnrpc.ChannelPoint @@ -353,7 +353,18 @@ func (b *blindedForwardTest) setupNetwork(ctx context.Context, if withInterceptor { carolArgs = append(carolArgs, "--requireinterceptor") } - b.carol = b.ht.NewNode("Carol", carolArgs) + + daveArgs := []string{"--bitcoin.timelockdelta=18"} + cfgs := [][]string{nil, nil, carolArgs, daveArgs} + param := lntest.OpenChannelParams{ + Amt: chanAmt, + } + + // Creates a network with the following topology and liquidity: + // Alice (100k)----- Bob (100k) ----- Carol (100k) ----- Dave + chanPoints, nodes := b.ht.CreateSimpleNetwork(cfgs, param) + b.channels = chanPoints + b.alice, b.bob, b.carol, b.dave = nodes[0], nodes[1], nodes[2], nodes[3] if withInterceptor { var err error @@ -362,10 +373,6 @@ func (b *blindedForwardTest) setupNetwork(ctx context.Context, ) require.NoError(b.ht, err, "interceptor") } - - b.dave = b.ht.NewNode("Dave", []string{"--bitcoin.timelockdelta=18"}) - - b.channels = setupFourHopNetwork(b.ht, b.carol, b.dave) } // buildBlindedPath returns a blinded route from Bob -> Carol -> Dave, with Bob @@ -395,7 +402,7 @@ func (b *blindedForwardTest) buildBlindedPath() *lnrpc.BlindedPaymentPath { require.Len(b.ht, payReq.BlindedPaths, 1) path := payReq.BlindedPaths[0].BlindedPath require.Len(b.ht, path.BlindedHops, 3) - require.EqualValues(b.ht, path.IntroductionNode, b.ht.Bob.PubKey[:]) + require.EqualValues(b.ht, path.IntroductionNode, b.bob.PubKey[:]) return payReq.BlindedPaths[0] } @@ -403,8 +410,8 @@ func (b *blindedForwardTest) buildBlindedPath() *lnrpc.BlindedPaymentPath { // cleanup tears down all channels created by the test and cancels the top // level context used in the test. func (b *blindedForwardTest) cleanup() { - b.ht.CloseChannel(b.ht.Alice, b.channels[0]) - b.ht.CloseChannel(b.ht.Bob, b.channels[1]) + b.ht.CloseChannel(b.alice, b.channels[0]) + b.ht.CloseChannel(b.bob, b.channels[1]) b.ht.CloseChannel(b.carol, b.channels[2]) b.cancel() @@ -431,7 +438,7 @@ func (b *blindedForwardTest) createRouteToBlinded(paymentAmt int64, }, } - resp := b.ht.Alice.RPC.QueryRoutes(req) + resp := b.alice.RPC.QueryRoutes(req) require.Greater(b.ht, len(resp.Routes), 0, "no routes") require.Len(b.ht, resp.Routes[0].Hops, 3, "unexpected route length") @@ -452,7 +459,7 @@ func (b *blindedForwardTest) sendBlindedPayment(ctx context.Context, ctx, cancel := context.WithTimeout(ctx, time.Hour) go func() { - _, err := b.ht.Alice.RPC.Router.SendToRouteV2(ctx, sendReq) + _, err := b.alice.RPC.Router.SendToRouteV2(ctx, sendReq) // We may get a context canceled error when the test is // finished. @@ -481,7 +488,7 @@ func (b *blindedForwardTest) sendToRoute(route *lnrpc.Route, // Let Alice send to the blinded payment path and assert that it // succeeds/fails. - htlcAttempt := b.ht.Alice.RPC.SendToRouteV2(sendReq) + htlcAttempt := b.alice.RPC.SendToRouteV2(sendReq) if assertSuccess { require.Nil(b.ht, htlcAttempt.Failure) require.Equal(b.ht, htlcAttempt.Status, @@ -498,7 +505,7 @@ func (b *blindedForwardTest) sendToRoute(route *lnrpc.Route, require.NoError(b.ht, err) pmt := b.ht.AssertPaymentStatus( - b.ht.Alice, preimage, lnrpc.Payment_FAILED, + b.alice, preimage, lnrpc.Payment_FAILED, ) require.Len(b.ht, pmt.Htlcs, 1) @@ -520,7 +527,7 @@ func (b *blindedForwardTest) drainCarolLiquidity(incoming bool) { receivingNode := b.dave if incoming { - sendingNode = b.ht.Bob + sendingNode = b.bob receivingNode = b.carol } @@ -611,8 +618,6 @@ func setupFourHopNetwork(ht *lntest.HarnessTest, // path and forward payments in a blinded route and finally, receiving the // payment. func testBlindedRouteInvoices(ht *lntest.HarnessTest) { - alice := ht.Alice - ctx, testCase := newBlindedForwardTest(ht) defer testCase.cleanup() @@ -620,6 +625,8 @@ func testBlindedRouteInvoices(ht *lntest.HarnessTest) { // blinded path that uses Bob as an introduction node. testCase.setupNetwork(ctx, false) + alice := testCase.alice + // Let Dave add a blinded invoice. // Add restrictions so that he only ever creates a single blinded path // from Bob to himself. @@ -737,14 +744,14 @@ func testRelayingBlindedError(ht *lntest.HarnessTest) { // over Alice -- Bob -- Carol -- Dave, where Bob is the introduction node and // has insufficient outgoing liquidity to forward on to carol. func testIntroductionNodeError(ht *lntest.HarnessTest) { - bob := ht.Bob - ctx, testCase := newBlindedForwardTest(ht) defer testCase.cleanup() testCase.setupNetwork(ctx, false) blindedPaymentPath := testCase.buildBlindedPath() route := testCase.createRouteToBlinded(10_000_000, blindedPaymentPath) + bob := testCase.bob + // Before we send our payment, drain all of Carol's incoming liquidity // so that she can't receive the forward from Bob, causing a failure // at the introduction node. @@ -770,8 +777,6 @@ func testIntroductionNodeError(ht *lntest.HarnessTest) { // testDisableIntroductionNode tests disabling of blinded forwards for the // introduction node. func testDisableIntroductionNode(ht *lntest.HarnessTest) { - alice, bob := ht.Alice, ht.Bob - // First construct a blinded route while Bob is still advertising the // route blinding feature bit to ensure that Bob is included in the // blinded path that Dave selects. @@ -781,6 +786,8 @@ func testDisableIntroductionNode(ht *lntest.HarnessTest) { blindedPaymentPath := testCase.buildBlindedPath() route := testCase.createRouteToBlinded(10_000_000, blindedPaymentPath) + alice, bob := testCase.alice, testCase.bob + // Now, disable route blinding for Bob, then re-connect to Alice. ht.RestartNodeWithExtraArgs(bob, []string{ "--protocol.no-route-blinding", @@ -796,8 +803,6 @@ func testDisableIntroductionNode(ht *lntest.HarnessTest) { // to resolve blinded HTLCs on chain between restarts, as we've got all the // infrastructure in place already for error testing. func testErrorHandlingOnChainFailure(ht *lntest.HarnessTest) { - alice, bob := ht.Alice, ht.Bob - // Setup a test case, note that we don't use its built in clean up // because we're going to close a channel, so we'll close out the // rest manually. @@ -811,6 +816,8 @@ func testErrorHandlingOnChainFailure(ht *lntest.HarnessTest) { 50_000_000, blindedPaymentPath, ) + alice, bob := testCase.alice, testCase.bob + // Once our interceptor is set up, we can send the blinded payment. cancelPmt := testCase.sendBlindedPayment(ctx, blindedRoute) defer cancelPmt() @@ -917,8 +924,8 @@ func testErrorHandlingOnChainFailure(ht *lntest.HarnessTest) { func testMPPToSingleBlindedPath(ht *lntest.HarnessTest) { // Create a five-node context consisting of Alice, Bob and three new // nodes. - alice, bob := ht.Alice, ht.Bob - + alice := ht.NewNode("Alice", nil) + bob := ht.NewNode("Bob", nil) dave := ht.NewNode("dave", nil) carol := ht.NewNode("carol", nil) eve := ht.NewNode("eve", nil) @@ -932,10 +939,12 @@ func testMPPToSingleBlindedPath(ht *lntest.HarnessTest) { // Send coins to the nodes and mine 1 blocks to confirm them. for i := 0; i < 2; i++ { + ht.FundCoinsUnconfirmed(btcutil.SatoshiPerBitcoin, alice) + ht.FundCoinsUnconfirmed(btcutil.SatoshiPerBitcoin, bob) ht.FundCoinsUnconfirmed(btcutil.SatoshiPerBitcoin, carol) ht.FundCoinsUnconfirmed(btcutil.SatoshiPerBitcoin, dave) ht.FundCoinsUnconfirmed(btcutil.SatoshiPerBitcoin, eve) - ht.MineBlocksAndAssertNumTxes(1, 3) + ht.MineBlocksAndAssertNumTxes(1, 5) } const paymentAmt = btcutil.Amount(300000) @@ -1107,11 +1116,11 @@ func testMPPToSingleBlindedPath(ht *lntest.HarnessTest) { // between him and the introduction node. So we expect that Carol is chosen as // the intro node and that one dummy hops is appended. func testBlindedRouteDummyHops(ht *lntest.HarnessTest) { - alice, bob := ht.Alice, ht.Bob + alice := ht.NewNodeWithCoins("Alice", nil) // Disable route blinding for Bob so that he is never chosen as the // introduction node. - ht.RestartNodeWithExtraArgs(bob, []string{ + bob := ht.NewNodeWithCoins("Bob", []string{ "--protocol.no-route-blinding", }) @@ -1278,7 +1287,8 @@ func testBlindedRouteDummyHops(ht *lntest.HarnessTest) { // \ / // --- Carol --- func testMPPToMultipleBlindedPaths(ht *lntest.HarnessTest) { - alice, bob := ht.Alice, ht.Bob + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNodeWithCoins("Bob", nil) // Create a four-node context consisting of Alice, Bob and three new // nodes. @@ -1440,8 +1450,6 @@ func testMPPToMultipleBlindedPaths(ht *lntest.HarnessTest) { // UpdateAddHTLC which we need to ensure gets included in the message on // restart. func testBlindedPaymentHTLCReForward(ht *lntest.HarnessTest) { - alice, bob := ht.Alice, ht.Bob - // Setup a test case. ctx, testCase := newBlindedForwardTest(ht) defer testCase.cleanup() @@ -1449,6 +1457,8 @@ func testBlindedPaymentHTLCReForward(ht *lntest.HarnessTest) { // Set up network with carol interceptor. testCase.setupNetwork(ctx, true) + alice, bob := testCase.alice, testCase.bob + // Let dave create invoice. blindedPaymentPath := testCase.buildBlindedPath() route := testCase.createRouteToBlinded(10_000_000, blindedPaymentPath) @@ -1465,7 +1475,7 @@ func testBlindedPaymentHTLCReForward(ht *lntest.HarnessTest) { go func() { defer close(done) - htlcAttempt, err := testCase.ht.Alice.RPC.Router.SendToRouteV2( + htlcAttempt, err := testCase.alice.RPC.Router.SendToRouteV2( ctx, sendReq, ) require.NoError(testCase.ht, err) diff --git a/itest/lnd_routing_test.go b/itest/lnd_routing_test.go index 04500a47cd..e05bf441b2 100644 --- a/itest/lnd_routing_test.go +++ b/itest/lnd_routing_test.go @@ -317,9 +317,8 @@ func runMultiHopSendToRoute(ht *lntest.HarnessTest, useGraphCache bool) { opts = append(opts, "--db.no-graph-cache") } - alice, bob := ht.Alice, ht.Bob - ht.RestartNodeWithExtraArgs(alice, opts) - + alice := ht.NewNodeWithCoins("Alice", opts) + bob := ht.NewNodeWithCoins("Bob", opts) ht.EnsureConnected(alice, bob) const chanAmt = btcutil.Amount(100000) @@ -417,7 +416,10 @@ func testSendToRouteErrorPropagation(ht *lntest.HarnessTest) { // Open a channel with 100k satoshis between Alice and Bob with Alice // being the sole funder of the channel. - alice, bob := ht.Alice, ht.Bob + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNode("Bob", nil) + ht.EnsureConnected(alice, bob) + chanPointAlice := ht.OpenChannel( alice, bob, lntest.OpenChannelParams{Amt: chanAmt}, ) @@ -496,7 +498,10 @@ func testPrivateChannels(ht *lntest.HarnessTest) { // where the 100k channel between Carol and Alice is private. // Open a channel with 200k satoshis between Alice and Bob. - alice, bob := ht.Alice, ht.Bob + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNode("Bob", nil) + ht.EnsureConnected(alice, bob) + chanPointAlice := ht.OpenChannel( alice, bob, lntest.OpenChannelParams{Amt: chanAmt * 2}, ) @@ -618,7 +623,10 @@ func testInvoiceRoutingHints(ht *lntest.HarnessTest) { // throughout this test. We'll include a push amount since we currently // require channels to have enough remote balance to cover the // invoice's payment. - alice, bob := ht.Alice, ht.Bob + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNodeWithCoins("Bob", nil) + ht.EnsureConnected(alice, bob) + chanPointBob := ht.OpenChannel( alice, bob, lntest.OpenChannelParams{ Amt: chanAmt, @@ -750,7 +758,7 @@ func testInvoiceRoutingHints(ht *lntest.HarnessTest) { // testScidAliasRoutingHints tests that dynamically created aliases via the RPC // are properly used when routing. func testScidAliasRoutingHints(ht *lntest.HarnessTest) { - bob := ht.Bob + bob := ht.NewNodeWithCoins("Bob", nil) const chanAmt = btcutil.Amount(800000) @@ -948,7 +956,10 @@ func testMultiHopOverPrivateChannels(ht *lntest.HarnessTest) { // First, we'll open a private channel between Alice and Bob with Alice // being the funder. - alice, bob := ht.Alice, ht.Bob + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNodeWithCoins("Bob", nil) + ht.EnsureConnected(alice, bob) + chanPointAlice := ht.OpenChannel( alice, bob, lntest.OpenChannelParams{ Amt: chanAmt, @@ -958,7 +969,7 @@ func testMultiHopOverPrivateChannels(ht *lntest.HarnessTest) { // Next, we'll create Carol's node and open a public channel between // her and Bob with Bob being the funder. - carol := ht.NewNode("Carol", nil) + carol := ht.NewNodeWithCoins("Carol", nil) ht.ConnectNodes(bob, carol) chanPointBob := ht.OpenChannel( bob, carol, lntest.OpenChannelParams{ @@ -973,7 +984,6 @@ func testMultiHopOverPrivateChannels(ht *lntest.HarnessTest) { // him and Carol with Carol being the funder. dave := ht.NewNode("Dave", nil) ht.ConnectNodes(carol, dave) - ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) chanPointCarol := ht.OpenChannel( carol, dave, lntest.OpenChannelParams{ @@ -1050,7 +1060,9 @@ func testQueryRoutes(ht *lntest.HarnessTest) { const chanAmt = btcutil.Amount(100000) // Grab Alice and Bob from the standby nodes. - alice, bob := ht.Alice, ht.Bob + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNodeWithCoins("Bob", nil) + ht.EnsureConnected(alice, bob) // Create Carol and connect her to Bob. We also send her some coins for // channel opening. @@ -1353,7 +1365,10 @@ func testRouteFeeCutoff(ht *lntest.HarnessTest) { const chanAmt = btcutil.Amount(100000) // Open a channel between Alice and Bob. - alice, bob := ht.Alice, ht.Bob + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNodeWithCoins("Bob", nil) + ht.EnsureConnected(alice, bob) + chanPointAliceBob := ht.OpenChannel( alice, bob, lntest.OpenChannelParams{Amt: chanAmt}, ) diff --git a/itest/lnd_signer_test.go b/itest/lnd_signer_test.go index 2858e771c5..ada7534089 100644 --- a/itest/lnd_signer_test.go +++ b/itest/lnd_signer_test.go @@ -25,7 +25,7 @@ import ( // the node's pubkey and a customized public key to check the validity of the // result. func testDeriveSharedKey(ht *lntest.HarnessTest) { - alice := ht.Alice + alice := ht.NewNode("Alice", nil) runDeriveSharedKey(ht, alice) } @@ -199,7 +199,7 @@ func runDeriveSharedKey(ht *lntest.HarnessTest, alice *node.HarnessNode) { // testSignOutputRaw makes sure that the SignOutputRaw RPC can be used with all // custom ways of specifying the signing key in the key descriptor/locator. func testSignOutputRaw(ht *lntest.HarnessTest) { - alice := ht.Alice + alice := ht.NewNodeWithCoins("Alice", nil) runSignOutputRaw(ht, alice) } @@ -381,7 +381,7 @@ func assertSignOutputRaw(ht *lntest.HarnessTest, // all custom flags by verifying with VerifyMessage. Tests both ECDSA and // Schnorr signatures. func testSignVerifyMessage(ht *lntest.HarnessTest) { - alice := ht.Alice + alice := ht.NewNode("Alice", nil) runSignVerifyMessage(ht, alice) } diff --git a/itest/lnd_single_hop_invoice_test.go b/itest/lnd_single_hop_invoice_test.go index 8051f7bb86..4adc37b72e 100644 --- a/itest/lnd_single_hop_invoice_test.go +++ b/itest/lnd_single_hop_invoice_test.go @@ -18,10 +18,11 @@ func testSingleHopInvoice(ht *lntest.HarnessTest) { // Open a channel with 100k satoshis between Alice and Bob with Alice // being the sole funder of the channel. chanAmt := btcutil.Amount(100000) - alice, bob := ht.Alice, ht.Bob - cp := ht.OpenChannel( - alice, bob, lntest.OpenChannelParams{Amt: chanAmt}, + chanPoints, nodes := ht.CreateSimpleNetwork( + [][]string{nil, nil}, lntest.OpenChannelParams{Amt: chanAmt}, ) + cp := chanPoints[0] + alice, bob := nodes[0], nodes[1] // assertAmountPaid is a helper closure that asserts the amount paid by // Alice and received by Bob are expected. diff --git a/itest/lnd_sweep_test.go b/itest/lnd_sweep_test.go index 025a6fee4b..2a226edf13 100644 --- a/itest/lnd_sweep_test.go +++ b/itest/lnd_sweep_test.go @@ -1548,7 +1548,7 @@ func testSweepCommitOutputAndAnchor(ht *lntest.HarnessTest) { // CPFP, then RBF. Along the way, we check the `BumpFee` can properly update // the fee function used by supplying new params. func testBumpFee(ht *lntest.HarnessTest) { - alice := ht.Alice + alice := ht.NewNodeWithCoins("Alice", nil) runBumpFee(ht, alice) } diff --git a/itest/lnd_switch_test.go b/itest/lnd_switch_test.go index f1ac628cab..ca68802fe0 100644 --- a/itest/lnd_switch_test.go +++ b/itest/lnd_switch_test.go @@ -383,7 +383,9 @@ func setupScenarioFourNodes(ht *lntest.HarnessTest) *scenarioFourNodes { } // Grab the standby nodes. - alice, bob := ht.Alice, ht.Bob + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNodeWithCoins("bob", nil) + ht.ConnectNodes(alice, bob) // As preliminary setup, we'll create two new nodes: Carol and Dave, // such that we now have a 4 node, 3 channel topology. Dave will make diff --git a/itest/lnd_taproot_test.go b/itest/lnd_taproot_test.go index d2dd35b528..2b68105fdb 100644 --- a/itest/lnd_taproot_test.go +++ b/itest/lnd_taproot_test.go @@ -49,7 +49,7 @@ var ( // testTaproot ensures that the daemon can send to and spend from taproot (p2tr) // outputs. func testTaproot(ht *lntest.HarnessTest) { - alice := ht.Alice + alice := ht.NewNode("Alice", nil) testTaprootSendCoinsKeySpendBip86(ht, alice) testTaprootComputeInputScriptKeySpendBip86(ht, alice) diff --git a/itest/lnd_trackpayments_test.go b/itest/lnd_trackpayments_test.go index b942cd4c72..0dd3467155 100644 --- a/itest/lnd_trackpayments_test.go +++ b/itest/lnd_trackpayments_test.go @@ -14,21 +14,19 @@ import ( // testTrackPayments tests whether a client that calls the TrackPayments api // receives payment updates. func testTrackPayments(ht *lntest.HarnessTest) { - alice, bob := ht.Alice, ht.Bob - - // Restart Alice with the new flag so she understands the new payment + // Create Alice with the new flag so she understands the new payment // status. - ht.RestartNodeWithExtraArgs(alice, []string{ - "--routerrpc.usestatusinitiated", - }) + cfgAlice := []string{"--routerrpc.usestatusinitiated"} + cfgs := [][]string{cfgAlice, nil} - // Open a channel between alice and bob. - ht.EnsureConnected(alice, bob) - channel := ht.OpenChannel( - alice, bob, lntest.OpenChannelParams{ + // Create a channel Alice->Bob. + chanPoints, nodes := ht.CreateSimpleNetwork( + cfgs, lntest.OpenChannelParams{ Amt: btcutil.Amount(300000), }, ) + channel := chanPoints[0] + alice, bob := nodes[0], nodes[1] // Call the TrackPayments api to listen for payment updates. req := &routerrpc.TrackPaymentsRequest{ @@ -104,12 +102,13 @@ func testTrackPayments(ht *lntest.HarnessTest) { // is not set, the new Payment_INITIATED is replaced with Payment_IN_FLIGHT. func testTrackPaymentsCompatible(ht *lntest.HarnessTest) { // Open a channel between alice and bob. - alice, bob := ht.Alice, ht.Bob - channel := ht.OpenChannel( - alice, bob, lntest.OpenChannelParams{ + chanPoints, nodes := ht.CreateSimpleNetwork( + [][]string{nil, nil}, lntest.OpenChannelParams{ Amt: btcutil.Amount(300000), }, ) + channel := chanPoints[0] + alice, bob := nodes[0], nodes[1] // Call the TrackPayments api to listen for payment updates. req := &routerrpc.TrackPaymentsRequest{ diff --git a/itest/lnd_wallet_import_test.go b/itest/lnd_wallet_import_test.go index f4acc6365b..eab0f2c04f 100644 --- a/itest/lnd_wallet_import_test.go +++ b/itest/lnd_wallet_import_test.go @@ -582,7 +582,7 @@ func runWalletImportAccountScenario(ht *lntest.HarnessTest, // Send coins to Carol's address and confirm them, making sure the // balance updates accordingly. - alice := ht.Alice + alice := ht.NewNodeWithCoins("Alice", nil) req := &lnrpc.SendCoinsRequest{ Addr: externalAddr, Amount: utxoAmt, @@ -694,7 +694,7 @@ func testWalletImportPubKeyScenario(ht *lntest.HarnessTest, addrType walletrpc.AddressType) { const utxoAmt int64 = btcutil.SatoshiPerBitcoin - alice := ht.Alice + alice := ht.NewNodeWithCoins("Alice", nil) // We'll start our test by having two nodes, Carol and Dave. // diff --git a/itest/lnd_watchtower_test.go b/itest/lnd_watchtower_test.go index 272d19d19d..1f5ad6ea31 100644 --- a/itest/lnd_watchtower_test.go +++ b/itest/lnd_watchtower_test.go @@ -39,7 +39,7 @@ var watchtowerTestCases = []*lntest.TestCase{ // testTowerClientTowerAndSessionManagement tests the various control commands // that a user has over the client's set of active towers and sessions. func testTowerClientTowerAndSessionManagement(ht *lntest.HarnessTest) { - alice := ht.Alice + alice := ht.NewNode("Alice", nil) const ( chanAmt = funding.MaxBtcFundingAmount @@ -240,7 +240,7 @@ func testTowerClientTowerAndSessionManagement(ht *lntest.HarnessTest) { // testTowerClientSessionDeletion tests that sessions are correctly deleted // when they are deemed closable. func testTowerClientSessionDeletion(ht *lntest.HarnessTest) { - alice := ht.Alice + alice := ht.NewNode("Alice", nil) const ( chanAmt = funding.MaxBtcFundingAmount @@ -395,7 +395,7 @@ func testRevokedCloseRetributionAltruistWatchtowerCase(ht *lntest.HarnessTest, // protection logic automatically. daveArgs := lntest.NodeArgsForCommitType(commitType) daveArgs = append(daveArgs, "--nolisten", "--wtclient.active") - dave := ht.NewNode("Dave", daveArgs) + dave := ht.NewNodeWithCoins("Dave", daveArgs) addTowerReq := &wtclientrpc.AddTowerRequest{ Pubkey: willyInfoPk, @@ -407,10 +407,6 @@ func testRevokedCloseRetributionAltruistWatchtowerCase(ht *lntest.HarnessTest, // announcement, so we open a channel with Carol, ht.ConnectNodes(dave, carol) - // Before we make a channel, we'll load up Dave with some coins sent - // directly from the miner. - ht.FundCoins(btcutil.SatoshiPerBitcoin, dave) - // Send one more UTXOs if this is a neutrino backend. if ht.IsNeutrinoBackend() { ht.FundCoins(btcutil.SatoshiPerBitcoin, dave) diff --git a/itest/lnd_wipe_fwdpkgs_test.go b/itest/lnd_wipe_fwdpkgs_test.go index b6211d7f8e..bd567f7a00 100644 --- a/itest/lnd_wipe_fwdpkgs_test.go +++ b/itest/lnd_wipe_fwdpkgs_test.go @@ -27,25 +27,12 @@ func testWipeForwardingPackages(ht *lntest.HarnessTest) { numInvoices = 3 ) - // Grab Alice and Bob from HarnessTest. - alice, bob := ht.Alice, ht.Bob - - // Create a new node Carol, which will create invoices that require - // Alice to pay. - carol := ht.NewNode("Carol", nil) - - // Connect Bob to Carol. - ht.ConnectNodes(bob, carol) - - // Open a channel between Alice and Bob. - chanPointAB := ht.OpenChannel( - alice, bob, lntest.OpenChannelParams{Amt: chanAmt}, - ) - - // Open a channel between Bob and Carol. - chanPointBC := ht.OpenChannel( - bob, carol, lntest.OpenChannelParams{Amt: chanAmt}, + chanPoints, nodes := ht.CreateSimpleNetwork( + [][]string{nil, nil, nil}, + lntest.OpenChannelParams{Amt: chanAmt}, ) + chanPointAB, chanPointBC := chanPoints[0], chanPoints[1] + alice, bob, carol := nodes[0], nodes[1], nodes[2] // Before we continue, make sure Alice has seen the channel between Bob // and Carol. diff --git a/itest/lnd_zero_conf_test.go b/itest/lnd_zero_conf_test.go index 6a658ab381..523a41c373 100644 --- a/itest/lnd_zero_conf_test.go +++ b/itest/lnd_zero_conf_test.go @@ -753,7 +753,7 @@ func testPrivateUpdateAlias(ht *lntest.HarnessTest, // testOptionScidUpgrade tests that toggling the option-scid-alias feature bit // correctly upgrades existing channels. func testOptionScidUpgrade(ht *lntest.HarnessTest) { - bob := ht.Bob + bob := ht.NewNodeWithCoins("Bob", nil) // Start carol with anchors only. carolArgs := []string{ diff --git a/lntest/harness.go b/lntest/harness.go index 5fb4be4dca..29940b6e04 100644 --- a/lntest/harness.go +++ b/lntest/harness.go @@ -603,6 +603,42 @@ func (h *HarnessTest) NewNode(name string, return node } +// NewNodeWithCoins creates a new node and asserts its creation. The node is +// guaranteed to have finished its initialization and all its subservers are +// started. In addition, 5 UTXO of 1 BTC each are sent to the node. +func (h *HarnessTest) NewNodeWithCoins(name string, + extraArgs []string) *node.HarnessNode { + + node, err := h.manager.newNode(h.T, name, extraArgs, nil, false) + require.NoErrorf(h, err, "unable to create new node for %s", name) + + // Start the node. + err = node.Start(h.runCtx) + require.NoError(h, err, "failed to start node %s", node.Name()) + + // Load up the wallets of the node with 5 outputs of 1 BTC each. + const ( + numOutputs = 5 + fundAmount = 1 * btcutil.SatoshiPerBitcoin + totalAmount = fundAmount * numOutputs + ) + + for i := 0; i < numOutputs; i++ { + h.createAndSendOutput( + node, fundAmount, + lnrpc.AddressType_WITNESS_PUBKEY_HASH, + ) + } + + // Mine a block to confirm the transactions. + h.MineBlocksAndAssertNumTxes(1, numOutputs) + + // Now block until the wallet have fully synced up. + h.WaitForBalanceConfirmed(node, totalAmount) + + return node +} + // Shutdown shuts down the given node and asserts that no errors occur. func (h *HarnessTest) Shutdown(node *node.HarnessNode) { // The process may not be in a state to always shutdown immediately, so From 6a743f4ed8866470d024c5e64b33b13ca63c0d68 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Wed, 20 Nov 2024 14:48:35 +0800 Subject: [PATCH 098/153] itest: remove unused method `setupFourHopNetwork` --- itest/lnd_route_blinding_test.go | 58 -------------------------------- 1 file changed, 58 deletions(-) diff --git a/itest/lnd_route_blinding_test.go b/itest/lnd_route_blinding_test.go index f0ef2e4a0a..c1811c4665 100644 --- a/itest/lnd_route_blinding_test.go +++ b/itest/lnd_route_blinding_test.go @@ -555,64 +555,6 @@ func (b *blindedForwardTest) drainCarolLiquidity(incoming bool) { b.ht.AssertPaymentStatusFromStream(pmtClient, lnrpc.Payment_SUCCEEDED) } -// setupFourHopNetwork creates a network with the following topology and -// liquidity: -// Alice (100k)----- Bob (100k) ----- Carol (100k) ----- Dave -// -// The funding outpoint for AB / BC / CD are returned in-order. -func setupFourHopNetwork(ht *lntest.HarnessTest, - carol, dave *node.HarnessNode) []*lnrpc.ChannelPoint { - - alice, bob := ht.Alice, ht.Bob - - const chanAmt = btcutil.Amount(100000) - var networkChans []*lnrpc.ChannelPoint - - // Open a channel with 100k satoshis between Alice and Bob with Alice - // being the sole funder of the channel. - chanPointAlice := ht.OpenChannel( - alice, bob, lntest.OpenChannelParams{ - Amt: chanAmt, - }, - ) - networkChans = append(networkChans, chanPointAlice) - - // Create a channel between bob and carol. - ht.EnsureConnected(bob, carol) - chanPointBob := ht.OpenChannel( - bob, carol, lntest.OpenChannelParams{ - Amt: chanAmt, - }, - ) - networkChans = append(networkChans, chanPointBob) - - // Fund carol and connect her and dave so that she can create a channel - // between them. - ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) - ht.EnsureConnected(carol, dave) - - chanPointCarol := ht.OpenChannel( - carol, dave, lntest.OpenChannelParams{ - Amt: chanAmt, - }, - ) - networkChans = append(networkChans, chanPointCarol) - - // Wait for all nodes to have seen all channels. - nodes := []*node.HarnessNode{alice, bob, carol, dave} - for _, chanPoint := range networkChans { - for _, node := range nodes { - ht.AssertChannelInGraph(node, chanPoint) - } - } - - return []*lnrpc.ChannelPoint{ - chanPointAlice, - chanPointBob, - chanPointCarol, - } -} - // testBlindedRouteInvoices tests lnd's ability to create a blinded payment path // which it then inserts into an invoice, sending to an invoice with a blinded // path and forward payments in a blinded route and finally, receiving the From b12a16fd279445b68ab3f5dd661e2038c77074dd Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Wed, 20 Nov 2024 14:51:37 +0800 Subject: [PATCH 099/153] itest+lntest: remove standby nodes This commit removes the standby nodes Alice and Bob. --- itest/lnd_test.go | 22 +--- lntest/harness.go | 177 ++------------------------------- lntest/harness_node_manager.go | 13 +-- 3 files changed, 14 insertions(+), 198 deletions(-) diff --git a/itest/lnd_test.go b/itest/lnd_test.go index f2a345fa11..ebc751ab28 100644 --- a/itest/lnd_test.go +++ b/itest/lnd_test.go @@ -8,7 +8,6 @@ import ( "os" "path/filepath" "runtime" - "strings" "testing" "time" @@ -110,10 +109,6 @@ func TestLightningNetworkDaemon(t *testing.T) { ) defer harnessTest.Stop() - // Setup standby nodes, Alice and Bob, which will be alive and shared - // among all the test cases. - harnessTest.SetupStandbyNodes() - // Get the current block height. height := harnessTest.CurrentHeight() @@ -130,22 +125,7 @@ func TestLightningNetworkDaemon(t *testing.T) { // avoid overwriting the external harness test that is // tied to the parent test. ht := harnessTest.Subtest(t1) - - // TODO(yy): split log files. - cleanTestCaseName := strings.ReplaceAll( - testCase.Name, " ", "_", - ) - ht.SetTestName(cleanTestCaseName) - - logLine := fmt.Sprintf( - "STARTING ============ %v ============\n", - testCase.Name, - ) - - ht.Alice.AddToLogf(logLine) - ht.Bob.AddToLogf(logLine) - - ht.EnsureConnected(ht.Alice, ht.Bob) + ht.SetTestName(testCase.Name) ht.RunTestCase(testCase) }) diff --git a/lntest/harness.go b/lntest/harness.go index 29940b6e04..5b146a6eca 100644 --- a/lntest/harness.go +++ b/lntest/harness.go @@ -4,6 +4,7 @@ import ( "context" "encoding/hex" "fmt" + "strings" "testing" "time" @@ -67,24 +68,12 @@ type TestCase struct { TestFunc func(t *HarnessTest) } -// standbyNodes are a list of nodes which are created during the initialization -// of the test and used across all test cases. -type standbyNodes struct { - // Alice and Bob are the initial seeder nodes that are automatically - // created to be the initial participants of the test network. - Alice *node.HarnessNode - Bob *node.HarnessNode -} - // HarnessTest builds on top of a testing.T with enhanced error detection. It // is responsible for managing the interactions among different nodes, and // providing easy-to-use assertions. type HarnessTest struct { *testing.T - // Embed the standbyNodes so we can easily access them via `ht.Alice`. - standbyNodes - // miner is a reference to a running full node that can be used to // create new blocks on the network. miner *miner.HarnessMiner @@ -271,97 +260,6 @@ func (h *HarnessTest) createAndSendOutput(target *node.HarnessNode, h.miner.SendOutput(output, defaultMinerFeeRate) } -// SetupRemoteSigningStandbyNodes starts the initial seeder nodes within the -// test harness in a remote signing configuration. The initial node's wallets -// will be funded wallets with 100x1 BTC outputs each. -func (h *HarnessTest) SetupRemoteSigningStandbyNodes() { - h.Log("Setting up standby nodes Alice and Bob with remote " + - "signing configurations...") - defer h.Log("Finished the setup, now running tests...") - - password := []byte("itestpassword") - - // Setup remote signing nodes for Alice and Bob. - signerAlice := h.NewNode("SignerAlice", nil) - signerBob := h.NewNode("SignerBob", nil) - - // Setup watch-only nodes for Alice and Bob, each configured with their - // own remote signing instance. - h.Alice = h.setupWatchOnlyNode("Alice", signerAlice, password) - h.Bob = h.setupWatchOnlyNode("Bob", signerBob, password) - - // Fund each node with 100 BTC (using 100 separate transactions). - const fundAmount = 1 * btcutil.SatoshiPerBitcoin - const numOutputs = 100 - const totalAmount = fundAmount * numOutputs - for _, node := range []*node.HarnessNode{h.Alice, h.Bob} { - h.manager.standbyNodes[node.Cfg.NodeID] = node - for i := 0; i < numOutputs; i++ { - h.createAndSendOutput( - node, fundAmount, - lnrpc.AddressType_WITNESS_PUBKEY_HASH, - ) - } - } - - // We generate several blocks in order to give the outputs created - // above a good number of confirmations. - const totalTxes = 200 - h.MineBlocksAndAssertNumTxes(numBlocksSendOutput, totalTxes) - - // Now we want to wait for the nodes to catch up. - h.WaitForBlockchainSync(h.Alice) - h.WaitForBlockchainSync(h.Bob) - - // Now block until both wallets have fully synced up. - h.WaitForBalanceConfirmed(h.Alice, totalAmount) - h.WaitForBalanceConfirmed(h.Bob, totalAmount) -} - -// SetUp starts the initial seeder nodes within the test harness. The initial -// node's wallets will be funded wallets with 10x10 BTC outputs each. -func (h *HarnessTest) SetupStandbyNodes() { - h.Log("Setting up standby nodes Alice and Bob...") - defer h.Log("Finished the setup, now running tests...") - - lndArgs := []string{ - "--default-remote-max-htlcs=483", - "--channel-max-fee-exposure=5000000", - } - - // Start the initial seeder nodes within the test network. - h.Alice = h.NewNode("Alice", lndArgs) - h.Bob = h.NewNode("Bob", lndArgs) - - // Load up the wallets of the seeder nodes with 100 outputs of 1 BTC - // each. - const fundAmount = 1 * btcutil.SatoshiPerBitcoin - const numOutputs = 100 - const totalAmount = fundAmount * numOutputs - for _, node := range []*node.HarnessNode{h.Alice, h.Bob} { - h.manager.standbyNodes[node.Cfg.NodeID] = node - for i := 0; i < numOutputs; i++ { - h.createAndSendOutput( - node, fundAmount, - lnrpc.AddressType_WITNESS_PUBKEY_HASH, - ) - } - } - - // We generate several blocks in order to give the outputs created - // above a good number of confirmations. - const totalTxes = 200 - h.MineBlocksAndAssertNumTxes(numBlocksSendOutput, totalTxes) - - // Now we want to wait for the nodes to catch up. - h.WaitForBlockchainSync(h.Alice) - h.WaitForBlockchainSync(h.Bob) - - // Now block until both wallets have fully synced up. - h.WaitForBalanceConfirmed(h.Alice, totalAmount) - h.WaitForBalanceConfirmed(h.Bob, totalAmount) -} - // Stop stops the test harness. func (h *HarnessTest) Stop() { // Do nothing if it's not started. @@ -399,24 +297,6 @@ func (h *HarnessTest) RunTestCase(testCase *TestCase) { testCase.TestFunc(h) } -// resetStandbyNodes resets all standby nodes by attaching the new testing.T -// and restarting them with the original config. -func (h *HarnessTest) resetStandbyNodes(t *testing.T) { - t.Helper() - - for _, hn := range h.manager.standbyNodes { - // Inherit the testing.T. - h.T = t - - // Reset the config so the node will be using the default - // config for the coming test. This will also inherit the - // test's running context. - h.RestartNodeWithExtraArgs(hn, hn.Cfg.OriginalExtraArgs) - - hn.AddToLogf("Finished test case %v", h.manager.currentTestCase) - } -} - // Subtest creates a child HarnessTest, which inherits the harness net and // stand by nodes created by the parent test. It will return a cleanup function // which resets all the standby nodes' configs back to its original state and @@ -428,7 +308,6 @@ func (h *HarnessTest) Subtest(t *testing.T) *HarnessTest { T: t, manager: h.manager, miner: h.miner, - standbyNodes: h.standbyNodes, feeService: h.feeService, lndErrorChan: make(chan error, lndErrorChanSize), } @@ -439,9 +318,6 @@ func (h *HarnessTest) Subtest(t *testing.T) *HarnessTest { // Inherit the subtest for the miner. st.miner.T = st.T - // Reset the standby nodes. - st.resetStandbyNodes(t) - // Reset fee estimator. st.feeService.Reset() @@ -471,14 +347,8 @@ func (h *HarnessTest) Subtest(t *testing.T) *HarnessTest { return } - // When we finish the test, reset the nodes' configs and take a - // snapshot of each of the nodes' internal states. - for _, node := range st.manager.standbyNodes { - st.cleanupStandbyNode(node) - } - // If found running nodes, shut them down. - st.shutdownNonStandbyNodes() + st.shutdownAllNodes() // We require the mempool to be cleaned from the test. require.Empty(st, st.miner.GetRawMempool(), "mempool not "+ @@ -498,26 +368,9 @@ func (h *HarnessTest) Subtest(t *testing.T) *HarnessTest { return st } -// shutdownNonStandbyNodes will shutdown any non-standby nodes. -func (h *HarnessTest) shutdownNonStandbyNodes() { - h.shutdownNodes(true) -} - // shutdownAllNodes will shutdown all running nodes. func (h *HarnessTest) shutdownAllNodes() { - h.shutdownNodes(false) -} - -// shutdownNodes will shutdown any non-standby nodes. If skipStandby is false, -// all the standby nodes will be shutdown too. -func (h *HarnessTest) shutdownNodes(skipStandby bool) { - for nid, node := range h.manager.activeNodes { - // If it's a standby node, skip. - _, ok := h.manager.standbyNodes[nid] - if ok && skipStandby { - continue - } - + for _, node := range h.manager.activeNodes { // The process may not be in a state to always shutdown // immediately, so we'll retry up to a hard limit to ensure we // eventually shutdown. @@ -566,26 +419,14 @@ func (h *HarnessTest) cleanupStandbyNode(hn *node.HarnessNode) { func (h *HarnessTest) removeConnectionns(hn *node.HarnessNode) { resp := hn.RPC.ListPeers() for _, peer := range resp.Peers { - // Skip disconnecting Alice and Bob. - switch peer.PubKey { - case h.Alice.PubKeyStr: - continue - case h.Bob.PubKeyStr: - continue - } - hn.RPC.DisconnectPeer(peer.PubKey) } } // SetTestName set the test case name. func (h *HarnessTest) SetTestName(name string) { - h.manager.currentTestCase = name - - // Overwrite the old log filename so we can create new log files. - for _, node := range h.manager.standbyNodes { - node.Cfg.LogFilenamePrefix = name - } + cleanTestCaseName := strings.ReplaceAll(name, " ", "_") + h.manager.currentTestCase = cleanTestCaseName } // NewNode creates a new node and asserts its creation. The node is guaranteed @@ -1507,7 +1348,7 @@ func (h *HarnessTest) fundCoins(amt btcutil.Amount, target *node.HarnessNode, } // FundCoins attempts to send amt satoshis from the internal mining node to the -// targeted lightning node using a P2WKH address. 2 blocks are mined after in +// targeted lightning node using a P2WKH address. 1 blocks are mined after in // order to confirm the transaction. func (h *HarnessTest) FundCoins(amt btcutil.Amount, hn *node.HarnessNode) { h.fundCoins(amt, hn, lnrpc.AddressType_WITNESS_PUBKEY_HASH, true) @@ -1783,9 +1624,9 @@ func (h *HarnessTest) RestartNodeAndRestoreDB(hn *node.HarnessNode) { // closures as the caller doesn't need to mine all the blocks to make sure the // mempool is empty. func (h *HarnessTest) CleanShutDown() { - // First, shutdown all non-standby nodes to prevent new transactions - // being created and fed into the mempool. - h.shutdownNonStandbyNodes() + // First, shutdown all nodes to prevent new transactions being created + // and fed into the mempool. + h.shutdownAllNodes() // Now mine blocks till the mempool is empty. h.cleanMempool() diff --git a/lntest/harness_node_manager.go b/lntest/harness_node_manager.go index 6ad4b90318..2040acec7f 100644 --- a/lntest/harness_node_manager.go +++ b/lntest/harness_node_manager.go @@ -40,10 +40,6 @@ type nodeManager struct { // {pubkey: *HarnessNode}. activeNodes map[uint32]*node.HarnessNode - // standbyNodes is a map of all the standby nodes, format: - // {pubkey: *HarnessNode}. - standbyNodes map[uint32]*node.HarnessNode - // nodeCounter is a monotonically increasing counter that's used as the // node's unique ID. nodeCounter atomic.Uint32 @@ -57,11 +53,10 @@ func newNodeManager(lndBinary string, dbBackend node.DatabaseBackend, nativeSQL bool) *nodeManager { return &nodeManager{ - lndBinary: lndBinary, - dbBackend: dbBackend, - nativeSQL: nativeSQL, - activeNodes: make(map[uint32]*node.HarnessNode), - standbyNodes: make(map[uint32]*node.HarnessNode), + lndBinary: lndBinary, + dbBackend: dbBackend, + nativeSQL: nativeSQL, + activeNodes: make(map[uint32]*node.HarnessNode), } } From 6d3ed892020fd3736170a9f23eda0faea9dbd878 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 29 Oct 2024 16:48:23 +0800 Subject: [PATCH 100/153] itest: remove unnecessary channel close and node shutdown Since we don't have standby nodes anymore, we don't need to close the channels when the test finishes. Previously we would do so to make sure the standby nodes have a clean state for the next test case, which is no longer relevant. --- itest/lnd_channel_balance_test.go | 7 -- itest/lnd_channel_funding_fund_max_test.go | 6 +- ...lnd_channel_funding_utxo_selection_test.go | 3 - itest/lnd_channel_graph_test.go | 18 +---- itest/lnd_channel_policy_test.go | 11 ---- itest/lnd_custom_features.go | 5 +- itest/lnd_forward_interceptor_test.go | 28 +------- itest/lnd_funding_test.go | 61 +++-------------- itest/lnd_hold_invoice_force_test.go | 3 - itest/lnd_hold_persistence_test.go | 6 +- itest/lnd_htlc_test.go | 1 - itest/lnd_invoice_acceptor_test.go | 6 +- itest/lnd_max_channel_size_test.go | 4 +- itest/lnd_max_htlcs_test.go | 5 +- itest/lnd_misc_test.go | 36 ++-------- itest/lnd_multi-hop-error-propagation_test.go | 8 --- itest/lnd_multi-hop-payments_test.go | 5 -- itest/lnd_network_test.go | 7 +- itest/lnd_onchain_test.go | 6 +- itest/lnd_open_channel_test.go | 50 ++------------ itest/lnd_payment_test.go | 56 +--------------- itest/lnd_psbt_test.go | 20 ------ itest/lnd_res_handoff_test.go | 2 - itest/lnd_rest_api_test.go | 3 +- itest/lnd_route_blinding_test.go | 9 --- itest/lnd_routing_test.go | 65 ++----------------- itest/lnd_single_hop_invoice_test.go | 2 - itest/lnd_switch_test.go | 20 +----- itest/lnd_trackpayments_test.go | 27 +------- itest/lnd_wumbo_channels_test.go | 3 +- itest/lnd_zero_conf_test.go | 3 - 31 files changed, 46 insertions(+), 440 deletions(-) diff --git a/itest/lnd_channel_balance_test.go b/itest/lnd_channel_balance_test.go index 82382e9110..f723b0492d 100644 --- a/itest/lnd_channel_balance_test.go +++ b/itest/lnd_channel_balance_test.go @@ -63,10 +63,6 @@ func testChannelBalance(ht *lntest.HarnessTest) { // Ensure Bob currently has no available balance within the channel. checkChannelBalance(bob, 0, amount-lntest.CalcStaticFee(cType, 0)) - - // Finally close the channel between Alice and Bob, asserting that the - // channel has been properly closed on-chain. - ht.CloseChannel(alice, chanPoint) } // testChannelUnsettledBalance will test that the UnsettledBalance field @@ -208,7 +204,4 @@ func testChannelUnsettledBalance(ht *lntest.HarnessTest) { // balance that equals to the amount of invoices * payAmt. The local // balance remains zero. checkChannelBalance(carol, 0, aliceLocal, numInvoices*payAmt, 0) - - // Force and assert the channel closure. - ht.ForceCloseChannel(alice, chanPointAlice) } diff --git a/itest/lnd_channel_funding_fund_max_test.go b/itest/lnd_channel_funding_fund_max_test.go index 43aec3c982..c93ab5fdc7 100644 --- a/itest/lnd_channel_funding_fund_max_test.go +++ b/itest/lnd_channel_funding_fund_max_test.go @@ -57,10 +57,7 @@ func testChannelFundMax(ht *lntest.HarnessTest) { // tests. args := lntest.NodeArgsForCommitType(lnrpc.CommitmentType_ANCHORS) alice := ht.NewNode("Alice", args) - defer ht.Shutdown(alice) - bob := ht.NewNode("Bob", args) - defer ht.Shutdown(bob) // Ensure both sides are connected so the funding flow can be properly // executed. @@ -229,13 +226,12 @@ func runFundMaxTestCase(ht *lntest.HarnessTest, alice, bob *node.HarnessNode, // Otherwise, if we expect to open a channel use the helper function. chanPoint := ht.OpenChannel(alice, bob, chanParams) + cType := ht.GetChannelCommitType(alice, chanPoint) // Close the channel between Alice and Bob, asserting // that the channel has been properly closed on-chain. defer ht.CloseChannel(alice, chanPoint) - cType := ht.GetChannelCommitType(alice, chanPoint) - // Alice's balance should be her amount subtracted by the commitment // transaction fee. checkChannelBalance( diff --git a/itest/lnd_channel_funding_utxo_selection_test.go b/itest/lnd_channel_funding_utxo_selection_test.go index 2b6d0cd301..f840b8ac99 100644 --- a/itest/lnd_channel_funding_utxo_selection_test.go +++ b/itest/lnd_channel_funding_utxo_selection_test.go @@ -64,10 +64,7 @@ func testChannelUtxoSelection(ht *lntest.HarnessTest) { // tests. args := lntest.NodeArgsForCommitType(lnrpc.CommitmentType_ANCHORS) alice := ht.NewNode("Alice", args) - defer ht.Shutdown(alice) - bob := ht.NewNode("Bob", args) - defer ht.Shutdown(bob) // Ensure both sides are connected so the funding flow can be properly // executed. diff --git a/itest/lnd_channel_graph_test.go b/itest/lnd_channel_graph_test.go index 0b27286459..a7ad37aca7 100644 --- a/itest/lnd_channel_graph_test.go +++ b/itest/lnd_channel_graph_test.go @@ -234,7 +234,7 @@ func testUnannouncedChannels(ht *lntest.HarnessTest) { // One block is enough to make the channel ready for use, since the // nodes have defaultNumConfs=1 set. - fundingChanPoint := ht.WaitForChannelOpenEvent(chanOpenUpdate) + ht.WaitForChannelOpenEvent(chanOpenUpdate) // Alice should have 1 edge in her graph. ht.AssertNumActiveEdges(alice, 1, true) @@ -248,9 +248,6 @@ func testUnannouncedChannels(ht *lntest.HarnessTest) { // Give the network a chance to learn that auth proof is confirmed. ht.AssertNumActiveEdges(alice, 1, false) - - // Close the channel used during the test. - ht.CloseChannel(alice, fundingChanPoint) } func testGraphTopologyNotifications(ht *lntest.HarnessTest) { @@ -367,9 +364,6 @@ func testGraphTopologyNtfns(ht *lntest.HarnessTest, pinned bool) { // Bob's new node announcement, and the channel between Bob and Carol. ht.AssertNumChannelUpdates(alice, chanPoint, 2) ht.AssertNumNodeAnns(alice, bob.PubKeyStr, 1) - - // Close the channel between Bob and Carol. - ht.CloseChannel(bob, chanPoint) } // testNodeAnnouncement ensures that when a node is started with one or more @@ -402,7 +396,7 @@ func testNodeAnnouncement(ht *lntest.HarnessTest) { // We'll then go ahead and open a channel between Bob and Dave. This // ensures that Alice receives the node announcement from Bob as part of // the announcement broadcast. - chanPoint := ht.OpenChannel( + ht.OpenChannel( bob, dave, lntest.OpenChannelParams{Amt: 1000000}, ) @@ -424,9 +418,6 @@ func testNodeAnnouncement(ht *lntest.HarnessTest) { allUpdates := ht.AssertNumNodeAnns(alice, dave.PubKeyStr, 1) nodeUpdate := allUpdates[len(allUpdates)-1] assertAddrs(nodeUpdate.Addresses, advertisedAddrs...) - - // Close the channel between Bob and Dave. - ht.CloseChannel(bob, chanPoint) } // testUpdateNodeAnnouncement ensures that the RPC endpoint validates @@ -531,7 +522,7 @@ func testUpdateNodeAnnouncement(ht *lntest.HarnessTest) { // Go ahead and open a channel between Bob and Dave. This // ensures that Alice receives the node announcement from Bob as part of // the announcement broadcast. - chanPoint := ht.OpenChannel( + ht.OpenChannel( bob, dave, lntest.OpenChannelParams{ Amt: 1000000, }, @@ -661,9 +652,6 @@ func testUpdateNodeAnnouncement(ht *lntest.HarnessTest) { FeatureUpdates: updateFeatureActions, } dave.RPC.UpdateNodeAnnouncementErr(nodeAnnReq) - - // Close the channel between Bob and Dave. - ht.CloseChannel(bob, chanPoint) } // assertSyncType asserts that the peer has an expected syncType. diff --git a/itest/lnd_channel_policy_test.go b/itest/lnd_channel_policy_test.go index 9c188fd95c..da9a2ac16d 100644 --- a/itest/lnd_channel_policy_test.go +++ b/itest/lnd_channel_policy_test.go @@ -419,11 +419,6 @@ func testUpdateChannelPolicy(ht *lntest.HarnessTest) { ht.AssertChannelPolicy( carol, alice.PubKeyStr, expectedPolicy, chanPoint3, ) - - // Close all channels. - ht.CloseChannel(alice, chanPoint) - ht.CloseChannel(bob, chanPoint2) - ht.CloseChannel(alice, chanPoint3) } // testSendUpdateDisableChannel ensures that a channel update with the disable @@ -772,10 +767,6 @@ func testUpdateChannelPolicyForPrivateChannel(ht *lntest.HarnessTest) { // Alice should have sent 20k satoshis + fee to Bob. ht.AssertAmountPaid("Alice(local) => Bob(remote)", alice, chanPointAliceBob, amtExpected, 0) - - // Finally, close the channels. - ht.CloseChannel(alice, chanPointAliceBob) - ht.CloseChannel(bob, chanPointBobCarol) } // testUpdateChannelPolicyFeeRateAccuracy tests that updating the channel policy @@ -844,8 +835,6 @@ func testUpdateChannelPolicyFeeRateAccuracy(ht *lntest.HarnessTest) { // Make sure that both Alice and Bob sees the same policy after update. assertNodesPolicyUpdate(ht, nodes, alice, expectedPolicy, chanPoint) - - ht.CloseChannel(alice, chanPoint) } // assertNodesPolicyUpdate checks that a given policy update has been received diff --git a/itest/lnd_custom_features.go b/itest/lnd_custom_features.go index cba6ee4aaf..701a86e79b 100644 --- a/itest/lnd_custom_features.go +++ b/itest/lnd_custom_features.go @@ -29,11 +29,10 @@ func testCustomFeatures(ht *lntest.HarnessTest) { } cfgs := [][]string{extraArgs, nil} - chanPoints, nodes := ht.CreateSimpleNetwork( + _, nodes := ht.CreateSimpleNetwork( cfgs, lntest.OpenChannelParams{Amt: 1000000}, ) alice, bob := nodes[0], nodes[1] - chanPoint := chanPoints[0] // Check that Alice's custom feature bit was sent to Bob in her init // message. @@ -79,8 +78,6 @@ func testCustomFeatures(ht *lntest.HarnessTest) { }, } alice.RPC.UpdateNodeAnnouncementErr(nodeAnnReq) - - ht.CloseChannel(alice, chanPoint) } // assertFeatureNotInSet checks that the features provided aren't contained in diff --git a/itest/lnd_forward_interceptor_test.go b/itest/lnd_forward_interceptor_test.go index 11fa01e038..b024df1020 100644 --- a/itest/lnd_forward_interceptor_test.go +++ b/itest/lnd_forward_interceptor_test.go @@ -177,10 +177,6 @@ func testForwardInterceptorDedupHtlc(ht *lntest.HarnessTest) { case <-time.After(defaultTimeout): require.Fail(ht, "timeout waiting for interceptor error") } - - // Finally, close channels. - ht.CloseChannel(alice, cpAB) - ht.CloseChannel(bob, cpBC) } // testForwardInterceptorBasic tests the forward interceptor RPC layer. @@ -345,10 +341,6 @@ func testForwardInterceptorBasic(ht *lntest.HarnessTest) { case <-time.After(defaultTimeout): require.Fail(ht, "timeout waiting for interceptor error") } - - // Finally, close channels. - ht.CloseChannel(alice, cpAB) - ht.CloseChannel(bob, cpBC) } // testForwardInterceptorModifiedHtlc tests that the interceptor can modify the @@ -367,7 +359,7 @@ func testForwardInterceptorModifiedHtlc(ht *lntest.HarnessTest) { {Local: bob, Remote: carol, Param: p}, } resp := ht.OpenMultiChannelsAsync(reqs) - cpAB, cpBC := resp[0], resp[1] + cpBC := resp[1] // Make sure Alice is aware of channel Bob=>Carol. ht.AssertChannelInGraph(alice, cpBC) @@ -451,10 +443,6 @@ func testForwardInterceptorModifiedHtlc(ht *lntest.HarnessTest) { var preimage lntypes.Preimage copy(preimage[:], invoice.RPreimage) ht.AssertPaymentStatus(alice, preimage, lnrpc.Payment_SUCCEEDED) - - // Finally, close channels. - ht.CloseChannel(alice, cpAB) - ht.CloseChannel(bob, cpBC) } // testForwardInterceptorWireRecords tests that the interceptor can read any @@ -475,7 +463,7 @@ func testForwardInterceptorWireRecords(ht *lntest.HarnessTest) { {Local: carol, Remote: dave, Param: p}, } resp := ht.OpenMultiChannelsAsync(reqs) - cpAB, cpBC, cpCD := resp[0], resp[1], resp[2] + cpBC := resp[1] // Make sure Alice is aware of channel Bob=>Carol. ht.AssertChannelInGraph(alice, cpBC) @@ -579,11 +567,6 @@ func testForwardInterceptorWireRecords(ht *lntest.HarnessTest) { return nil }, ) - - // Finally, close channels. - ht.CloseChannel(alice, cpAB) - ht.CloseChannel(bob, cpBC) - ht.CloseChannel(carol, cpCD) } // testForwardInterceptorRestart tests that the interceptor can read any wire @@ -605,7 +588,7 @@ func testForwardInterceptorRestart(ht *lntest.HarnessTest) { {Local: carol, Remote: dave, Param: p}, } resp := ht.OpenMultiChannelsAsync(reqs) - cpAB, cpBC, cpCD := resp[0], resp[1], resp[2] + cpBC, cpCD := resp[1], resp[2] // Make sure Alice is aware of channels Bob=>Carol and Carol=>Dave. ht.AssertChannelInGraph(alice, cpBC) @@ -739,11 +722,6 @@ func testForwardInterceptorRestart(ht *lntest.HarnessTest) { return nil }, ) - - // Finally, close channels. - ht.CloseChannel(alice, cpAB) - ht.CloseChannel(bob, cpBC) - ht.CloseChannel(carol, cpCD) } // interceptorTestScenario is a helper struct to hold the test context and diff --git a/itest/lnd_funding_test.go b/itest/lnd_funding_test.go index 022fedeafa..adb56fa123 100644 --- a/itest/lnd_funding_test.go +++ b/itest/lnd_funding_test.go @@ -91,7 +91,7 @@ func testBasicChannelFunding(ht *lntest.HarnessTest) { return } - carolChan, daveChan, closeChan := basicChannelFundingTest( + carolChan, daveChan := basicChannelFundingTest( ht, carol, dave, nil, privateChan, &carolCommitType, ) @@ -157,10 +157,6 @@ func testBasicChannelFunding(ht *lntest.HarnessTest) { "commit type %v, instead got "+ "%v", expType, chansCommitType) } - - // As we've concluded this sub-test case we'll now close out - // the channel for both sides. - closeChan() } test: @@ -195,7 +191,7 @@ test: func basicChannelFundingTest(ht *lntest.HarnessTest, alice, bob *node.HarnessNode, fundingShim *lnrpc.FundingShim, privateChan bool, commitType *lnrpc.CommitmentType) (*lnrpc.Channel, - *lnrpc.Channel, func()) { + *lnrpc.Channel) { chanAmt := funding.MaxBtcFundingAmount pushAmt := btcutil.Amount(100000) @@ -267,14 +263,7 @@ func basicChannelFundingTest(ht *lntest.HarnessTest, aliceChannel := ht.GetChannelByChanPoint(alice, chanPoint) bobChannel := ht.GetChannelByChanPoint(bob, chanPoint) - closeChan := func() { - // Finally, immediately close the channel. This function will - // also block until the channel is closed and will additionally - // assert the relevant channel closing post conditions. - ht.CloseChannel(alice, chanPoint) - } - - return aliceChannel, bobChannel, closeChan + return aliceChannel, bobChannel } // testUnconfirmedChannelFunding tests that our unconfirmed change outputs can @@ -382,25 +371,12 @@ func testUnconfirmedChannelFunding(ht *lntest.HarnessTest) { // spend and the funding tx. ht.MineBlocksAndAssertNumTxes(6, 2) - chanPoint := ht.WaitForChannelOpenEvent(chanOpenUpdate) + ht.WaitForChannelOpenEvent(chanOpenUpdate) // With the channel open, we'll check the balances on each side of the // channel as a sanity check to ensure things worked out as intended. checkChannelBalance(carol, carolLocalBalance, pushAmt, 0, 0) checkChannelBalance(alice, pushAmt, carolLocalBalance, 0, 0) - - // TODO(yy): remove the sleep once the following bug is fixed. - // - // We may get the error `unable to gracefully close channel while peer - // is offline (try force closing it instead): channel link not found`. - // This happens because the channel link hasn't been added yet but we - // now proceed to closing the channel. We may need to revisit how the - // channel open event is created and make sure the event is only sent - // after all relevant states have been updated. - time.Sleep(2 * time.Second) - - // Now that we're done with the test, the channel can be closed. - ht.CloseChannel(carol, chanPoint) } // testChannelFundingInputTypes tests that any type of supported input type can @@ -607,7 +583,7 @@ func runExternalFundingScriptEnforced(ht *lntest.HarnessTest) { // At this point, we'll now carry out the normal basic channel funding // test as everything should now proceed as normal (a regular channel // funding flow). - carolChan, daveChan, _ := basicChannelFundingTest( + carolChan, daveChan := basicChannelFundingTest( ht, carol, dave, fundingShim2, false, nil, ) @@ -723,7 +699,7 @@ func runExternalFundingTaproot(ht *lntest.HarnessTest) { // At this point, we'll now carry out the normal basic channel funding // test as everything should now proceed as normal (a regular channel // funding flow). - carolChan, daveChan, _ := basicChannelFundingTest( + carolChan, daveChan := basicChannelFundingTest( ht, carol, dave, fundingShim2, true, &commitmentType, ) @@ -936,11 +912,6 @@ func testChannelFundingPersistence(ht *lntest.HarnessTest) { shortChanID := lnwire.NewShortChanIDFromInt(chanAlice.ChanId) label = labels.MakeLabel(labels.LabelTypeChannelOpen, &shortChanID) require.Equal(ht, label, tx.Label, "open channel label not updated") - - // Finally, immediately close the channel. This function will also - // block until the channel is closed and will additionally assert the - // relevant channel closing post conditions. - ht.CloseChannel(alice, chanPoint) } // testBatchChanFunding makes sure multiple channels can be opened in one batch @@ -1133,15 +1104,6 @@ func testBatchChanFunding(ht *lntest.HarnessTest) { chainreg.DefaultBitcoinBaseFeeMSat, chainreg.DefaultBitcoinFeeRate, ) - - // To conclude, we'll close the newly created channel between Carol and - // Dave. This function will also block until the channel is closed and - // will additionally assert the relevant channel closing post - // conditions. - ht.CloseChannel(alice, chanPoint1) - ht.CloseChannel(alice, chanPoint2) - ht.CloseChannel(alice, chanPoint3) - ht.CloseChannel(alice, chanPoint4) } // ensurePolicy ensures that the peer sees alice's channel fee settings. @@ -1331,13 +1293,12 @@ func testChannelFundingWithUnstableUtxos(ht *lntest.HarnessTest) { // Open a channel to dave with an unconfirmed utxo. Although this utxo // is unconfirmed it can be used to open a channel because it did not // originated from the sweeper subsystem. - update := ht.OpenChannelAssertPending(carol, dave, + ht.OpenChannelAssertPending(carol, dave, lntest.OpenChannelParams{ Amt: chanSize, SpendUnconfirmed: true, CommitmentType: cType, }) - chanPoint1 := lntest.ChanPointFromPendingUpdate(update) // Verify that both nodes know about the channel. ht.AssertNumPendingOpenChannels(carol, 1) @@ -1349,7 +1310,7 @@ func testChannelFundingWithUnstableUtxos(ht *lntest.HarnessTest) { // so unconfirmed utxos originated from prior channel opening are safe // to use because channel opening should not be RBFed, at least not for // now. - update = ht.OpenChannelAssertPending(carol, dave, + update := ht.OpenChannelAssertPending(carol, dave, lntest.OpenChannelParams{ Amt: chanSize, SpendUnconfirmed: true, @@ -1468,20 +1429,16 @@ func testChannelFundingWithUnstableUtxos(ht *lntest.HarnessTest) { // Now after the sweep utxo is confirmed it is stable and can be used // for channel openings again. - update = ht.OpenChannelAssertPending(carol, dave, + ht.OpenChannelAssertPending(carol, dave, lntest.OpenChannelParams{ Amt: chanSize, SpendUnconfirmed: true, CommitmentType: cType, }) - chanPoint4 := lntest.ChanPointFromPendingUpdate(update) // Verify that both nodes know about the channel. ht.AssertNumPendingOpenChannels(carol, 1) ht.AssertNumPendingOpenChannels(dave, 1) ht.MineBlocksAndAssertNumTxes(1, 1) - - ht.CloseChannel(carol, chanPoint1) - ht.CloseChannel(carol, chanPoint4) } diff --git a/itest/lnd_hold_invoice_force_test.go b/itest/lnd_hold_invoice_force_test.go index 5f0a54670b..19e44a8375 100644 --- a/itest/lnd_hold_invoice_force_test.go +++ b/itest/lnd_hold_invoice_force_test.go @@ -140,7 +140,4 @@ func testHoldInvoiceForceClose(ht *lntest.HarnessTest) { // outgoing HTLCs in her channel as the only HTLC has already been // canceled. ht.AssertNumPendingForceClose(alice, 0) - - // Clean up the channel. - ht.CloseChannel(alice, chanPoint) } diff --git a/itest/lnd_hold_persistence_test.go b/itest/lnd_hold_persistence_test.go index 03c5911371..e8a273f022 100644 --- a/itest/lnd_hold_persistence_test.go +++ b/itest/lnd_hold_persistence_test.go @@ -35,7 +35,7 @@ func testHoldInvoicePersistence(ht *lntest.HarnessTest) { // Open a channel between Alice and Carol which is private so that we // cover the addition of hop hints for hold invoices. - chanPointAlice := ht.OpenChannel( + ht.OpenChannel( alice, carol, lntest.OpenChannelParams{ Amt: chanAmt, Private: true, @@ -222,8 +222,4 @@ func testHoldInvoicePersistence(ht *lntest.HarnessTest) { "wrong failure reason") } } - - // Finally, close all channels. - ht.CloseChannel(alice, chanPointBob) - ht.CloseChannel(alice, chanPointAlice) } diff --git a/itest/lnd_htlc_test.go b/itest/lnd_htlc_test.go index 001a4dc65a..e5825993f9 100644 --- a/itest/lnd_htlc_test.go +++ b/itest/lnd_htlc_test.go @@ -24,7 +24,6 @@ func testLookupHtlcResolution(ht *lntest.HarnessTest) { cp := ht.OpenChannel( alice, carol, lntest.OpenChannelParams{Amt: chanAmt}, ) - defer ht.CloseChannel(alice, cp) // Channel should be ready for payments. const payAmt = 100 diff --git a/itest/lnd_invoice_acceptor_test.go b/itest/lnd_invoice_acceptor_test.go index d27d13d9db..273402c67d 100644 --- a/itest/lnd_invoice_acceptor_test.go +++ b/itest/lnd_invoice_acceptor_test.go @@ -32,7 +32,7 @@ func testInvoiceHtlcModifierBasic(ht *lntest.HarnessTest) { {Local: bob, Remote: carol, Param: p}, } resp := ht.OpenMultiChannelsAsync(reqs) - cpAB, cpBC := resp[0], resp[1] + cpBC := resp[1] // Make sure Alice is aware of channel Bob=>Carol. ht.AssertChannelInGraph(alice, cpBC) @@ -204,10 +204,6 @@ func testInvoiceHtlcModifierBasic(ht *lntest.HarnessTest) { } cancelModifier() - - // Finally, close channels. - ht.CloseChannel(alice, cpAB) - ht.CloseChannel(bob, cpBC) } // acceptorTestCase is a helper struct to hold test case data. diff --git a/itest/lnd_max_channel_size_test.go b/itest/lnd_max_channel_size_test.go index 9959c75895..4e19f86538 100644 --- a/itest/lnd_max_channel_size_test.go +++ b/itest/lnd_max_channel_size_test.go @@ -56,9 +56,7 @@ func testMaxChannelSize(ht *lntest.HarnessTest) { // Creating a wumbo channel between these two nodes should succeed. ht.EnsureConnected(wumboNode, wumboNode3) - chanPoint := ht.OpenChannel( + ht.OpenChannel( wumboNode, wumboNode3, lntest.OpenChannelParams{Amt: chanAmt}, ) - - ht.CloseChannel(wumboNode, chanPoint) } diff --git a/itest/lnd_max_htlcs_test.go b/itest/lnd_max_htlcs_test.go index a22a946a76..6c4eb85946 100644 --- a/itest/lnd_max_htlcs_test.go +++ b/itest/lnd_max_htlcs_test.go @@ -25,14 +25,13 @@ func testMaxHtlcPathfind(ht *lntest.HarnessTest) { cfgs := [][]string{cfg, cfg} // Create a channel Alice->Bob. - chanPoints, nodes := ht.CreateSimpleNetwork( + _, nodes := ht.CreateSimpleNetwork( cfgs, lntest.OpenChannelParams{ Amt: 1000000, PushAmt: 800000, RemoteMaxHtlcs: uint16(maxHtlcs), }, ) - chanPoint := chanPoints[0] alice, bob := nodes[0], nodes[1] // Alice and bob should have one channel open with each other now. @@ -78,8 +77,6 @@ func testMaxHtlcPathfind(ht *lntest.HarnessTest) { ht.AssertNumActiveHtlcs(alice, 0) ht.AssertNumActiveHtlcs(bob, 0) - - ht.CloseChannel(alice, chanPoint) } type holdSubscription struct { diff --git a/itest/lnd_misc_test.go b/itest/lnd_misc_test.go index 70fcbe971b..f983b39e91 100644 --- a/itest/lnd_misc_test.go +++ b/itest/lnd_misc_test.go @@ -156,7 +156,6 @@ func testSphinxReplayPersistence(ht *lntest.HarnessTest) { Amt: chanAmt, }, ) - defer ht.CloseChannel(fred, chanPointFC) // Now that the channel is open, create an invoice for Dave which // expects a payment of 1000 satoshis from Carol paid via a particular @@ -225,9 +224,6 @@ func testSphinxReplayPersistence(ht *lntest.HarnessTest) { // unaltered. ht.AssertAmountPaid("carol => dave", carol, chanPoint, 0, 0) ht.AssertAmountPaid("dave <= carol", dave, chanPoint, 0, 0) - - // Cleanup by mining the force close and sweep transaction. - ht.ForceCloseChannel(carol, chanPoint) } // testListChannels checks that the response from ListChannels is correct. It @@ -420,12 +416,6 @@ func testMaxPendingChannels(ht *lntest.HarnessTest) { chanPoints[i] = fundingChanPoint } - - // Next, close the channel between Alice and Carol, asserting that the - // channel has been properly closed on-chain. - for _, chanPoint := range chanPoints { - ht.CloseChannel(alice, chanPoint) - } } // testGarbageCollectLinkNodes tests that we properly garbage collect link @@ -464,7 +454,7 @@ func testGarbageCollectLinkNodes(ht *lntest.HarnessTest) { dave := ht.NewNode("Dave", nil) ht.ConnectNodes(alice, dave) - persistentChanPoint := ht.OpenChannel( + ht.OpenChannel( alice, dave, lntest.OpenChannelParams{ Amt: chanAmt, }, @@ -537,9 +527,6 @@ func testGarbageCollectLinkNodes(ht *lntest.HarnessTest) { "did not expect to find bob in the channel graph, but did") require.NotContains(ht, channelGraph.Nodes, carol.PubKeyStr, "did not expect to find carol in the channel graph, but did") - - // Now that the test is done, we can also close the persistent link. - ht.CloseChannel(alice, persistentChanPoint) } // testRejectHTLC tests that a node can be created with the flag --rejecthtlc. @@ -566,14 +553,14 @@ func testRejectHTLC(ht *lntest.HarnessTest) { ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) // Open a channel between Alice and Carol. - chanPointAlice := ht.OpenChannel( + ht.OpenChannel( alice, carol, lntest.OpenChannelParams{ Amt: chanAmt, }, ) // Open a channel between Carol and Bob. - chanPointCarol := ht.OpenChannel( + ht.OpenChannel( carol, bob, lntest.OpenChannelParams{ Amt: chanAmt, }, @@ -636,10 +623,6 @@ func testRejectHTLC(ht *lntest.HarnessTest) { ) ht.AssertLastHTLCError(alice, lnrpc.Failure_CHANNEL_DISABLED) - - // Close all channels. - ht.CloseChannel(alice, chanPointAlice) - ht.CloseChannel(carol, chanPointCarol) } // testNodeSignVerify checks that only connected nodes are allowed to perform @@ -654,9 +637,8 @@ func testNodeSignVerify(ht *lntest.HarnessTest) { // Create a channel between alice and bob. cfgs := [][]string{nil, nil} - chanPoints, nodes := ht.CreateSimpleNetwork(cfgs, p) + _, nodes := ht.CreateSimpleNetwork(cfgs, p) alice, bob := nodes[0], nodes[1] - aliceBobCh := chanPoints[0] // alice signs "alice msg" and sends her signature to bob. aliceMsg := []byte("alice msg") @@ -684,9 +666,6 @@ func testNodeSignVerify(ht *lntest.HarnessTest) { require.False(ht, verifyResp.Valid, "carol's signature didn't validate") require.Equal(ht, verifyResp.Pubkey, carol.PubKeyStr, "carol's signature doesn't contain alice's pubkey.") - - // Close the channel between alice and bob. - ht.CloseChannel(alice, aliceBobCh) } // testAbandonChannel abandons a channel and asserts that it is no longer open @@ -702,7 +681,7 @@ func testAbandonChannel(ht *lntest.HarnessTest) { // Create a channel between alice and bob. cfgs := [][]string{nil, nil} chanPoints, nodes := ht.CreateSimpleNetwork(cfgs, channelParam) - alice, bob := nodes[0], nodes[1] + alice := nodes[0] chanPoint := chanPoints[0] // Now that the channel is open, we'll obtain its channel ID real quick @@ -755,11 +734,6 @@ func testAbandonChannel(ht *lntest.HarnessTest) { // Calling AbandonChannel again, should result in no new errors, as the // channel has already been removed. alice.RPC.AbandonChannel(abandonChannelRequest) - - // Now that we're done with the test, the channel can be closed. This - // is necessary to avoid unexpected outcomes of other tests that use - // Bob's lnd instance. - ht.ForceCloseChannel(bob, chanPoint) } // testSendAllCoins tests that we're able to properly sweep all coins from the diff --git a/itest/lnd_multi-hop-error-propagation_test.go b/itest/lnd_multi-hop-error-propagation_test.go index 12ec3430fb..8d20a8fc3b 100644 --- a/itest/lnd_multi-hop-error-propagation_test.go +++ b/itest/lnd_multi-hop-error-propagation_test.go @@ -365,12 +365,4 @@ func testHtlcErrorPropagation(ht *lntest.HarnessTest) { ht.AssertHtlcEventTypes( bobEvents, routerrpc.HtlcEvent_UNKNOWN, lntest.HtlcEventFinal, ) - - // Finally, immediately close the channel. This function will also - // block until the channel is closed and will additionally assert the - // relevant channel closing post conditions. - ht.CloseChannel(alice, chanPointAlice) - - // Force close Bob's final channel. - ht.ForceCloseChannel(bob, chanPointBob) } diff --git a/itest/lnd_multi-hop-payments_test.go b/itest/lnd_multi-hop-payments_test.go index 84c9800aba..e965140523 100644 --- a/itest/lnd_multi-hop-payments_test.go +++ b/itest/lnd_multi-hop-payments_test.go @@ -235,11 +235,6 @@ func testMultiHopPayments(ht *lntest.HarnessTest) { ht.AssertHtlcEvents( bobEvents, 0, 0, numPayments, 0, routerrpc.HtlcEvent_RECEIVE, ) - - // Finally, close all channels. - ht.CloseChannel(alice, chanPointAlice) - ht.CloseChannel(dave, chanPointDave) - ht.CloseChannel(carol, chanPointCarol) } // updateChannelPolicy updates the channel policy of node to the given fees and diff --git a/itest/lnd_network_test.go b/itest/lnd_network_test.go index ff305b59f4..44a0dcffa5 100644 --- a/itest/lnd_network_test.go +++ b/itest/lnd_network_test.go @@ -133,9 +133,7 @@ func testReconnectAfterIPChange(ht *lntest.HarnessTest) { // We'll then go ahead and open a channel between Alice and Dave. This // ensures that Charlie receives the node announcement from Alice as // part of the announcement broadcast. - chanPoint := ht.OpenChannel( - alice, dave, lntest.OpenChannelParams{Amt: 1000000}, - ) + ht.OpenChannel(alice, dave, lntest.OpenChannelParams{Amt: 1000000}) // waitForNodeAnnouncement is a closure used to wait on the given graph // subscription for a node announcement from a node with the given @@ -210,9 +208,6 @@ func testReconnectAfterIPChange(ht *lntest.HarnessTest) { // address to one not listed in Dave's original advertised list of // addresses. ht.AssertConnected(dave, charlie) - - // Finally, close the channel. - ht.CloseChannel(alice, chanPoint) } // testAddPeerConfig tests that the "--addpeer" config flag successfully adds diff --git a/itest/lnd_onchain_test.go b/itest/lnd_onchain_test.go index 8b14092558..3a32a8c6a5 100644 --- a/itest/lnd_onchain_test.go +++ b/itest/lnd_onchain_test.go @@ -153,7 +153,7 @@ func testChainKitSendOutputsAnchorReserve(ht *lntest.HarnessTest) { // Charlie opens an anchor channel and keeps twice the amount of the // anchor reserve in her wallet. chanAmt := fundingAmount - 2*btcutil.Amount(reserve.RequiredReserve) - outpoint := ht.OpenChannel(charlie, bob, lntest.OpenChannelParams{ + ht.OpenChannel(charlie, bob, lntest.OpenChannelParams{ Amt: chanAmt, CommitmentType: lnrpc.CommitmentType_ANCHORS, SatPerVByte: 1, @@ -207,11 +207,7 @@ func testChainKitSendOutputsAnchorReserve(ht *lntest.HarnessTest) { // This second transaction should be published correctly. charlie.RPC.SendOutputs(req) - ht.MineBlocksAndAssertNumTxes(1, 1) - - // Clean up our test setup. - ht.CloseChannel(charlie, outpoint) } // testAnchorReservedValue tests that we won't allow sending transactions when diff --git a/itest/lnd_open_channel_test.go b/itest/lnd_open_channel_test.go index 20f456ea9b..9fb41a2dea 100644 --- a/itest/lnd_open_channel_test.go +++ b/itest/lnd_open_channel_test.go @@ -4,7 +4,6 @@ import ( "fmt" "strings" "testing" - "time" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" @@ -119,8 +118,6 @@ func testOpenChannelAfterReorg(ht *lntest.HarnessTest) { // Cleanup by mining the funding tx again, then closing the channel. block = ht.MineBlocksAndAssertNumTxes(1, 1)[0] ht.AssertTxInBlock(block, *fundingTxID) - - ht.CloseChannel(alice, chanPoint) } // testOpenChannelFeePolicy checks if different channel fee scenarios are @@ -256,15 +253,13 @@ func testOpenChannelUpdateFeePolicy(ht *lntest.HarnessTest) { // Create a channel Alice->Bob. chanPoint := ht.OpenChannel(alice, bob, chanParams) - defer ht.CloseChannel(alice, chanPoint) // Create a channel Carol->Alice. - chanPoint2 := ht.OpenChannel( + ht.OpenChannel( carol, alice, lntest.OpenChannelParams{ Amt: 500000, }, ) - defer ht.CloseChannel(carol, chanPoint2) // Alice and Bob should see each other's ChannelUpdates, // advertising the preferred routing policies. @@ -542,13 +537,7 @@ func testUpdateOnFunderPendingOpenChannels(ht *lntest.HarnessTest) { Amt: funding.MaxBtcFundingAmount, PushAmt: funding.MaxBtcFundingAmount / 2, } - pendingChan := ht.OpenChannelAssertPending(alice, bob, params) - chanPoint := &lnrpc.ChannelPoint{ - FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{ - FundingTxidBytes: pendingChan.Txid, - }, - OutputIndex: pendingChan.OutputIndex, - } + ht.OpenChannelAssertPending(alice, bob, params) // Alice and Bob should both consider the channel pending open. ht.AssertNumPendingOpenChannels(alice, 1) @@ -598,9 +587,6 @@ func testUpdateOnFunderPendingOpenChannels(ht *lntest.HarnessTest) { // Once Alice sees the channel as active, she will process the cached // premature `update_add_htlc` and settles the payment. ht.AssertPaymentStatusFromStream(bobStream, lnrpc.Payment_SUCCEEDED) - - // Close the channel. - ht.CloseChannel(alice, chanPoint) } // testUpdateOnFundeePendingOpenChannels checks that when the funder sends an @@ -625,13 +611,7 @@ func testUpdateOnFundeePendingOpenChannels(ht *lntest.HarnessTest) { params := lntest.OpenChannelParams{ Amt: funding.MaxBtcFundingAmount, } - pendingChan := ht.OpenChannelAssertPending(alice, bob, params) - chanPoint := &lnrpc.ChannelPoint{ - FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{ - FundingTxidBytes: pendingChan.Txid, - }, - OutputIndex: pendingChan.OutputIndex, - } + ht.OpenChannelAssertPending(alice, bob, params) // Alice and Bob should both consider the channel pending open. ht.AssertNumPendingOpenChannels(alice, 1) @@ -681,9 +661,6 @@ func testUpdateOnFundeePendingOpenChannels(ht *lntest.HarnessTest) { // Once Bob sees the channel as active, he will process the cached // premature `update_add_htlc` and settles the payment. ht.AssertPaymentStatusFromStream(aliceStream, lnrpc.Payment_SUCCEEDED) - - // Close the channel. - ht.CloseChannel(alice, chanPoint) } // verifyCloseUpdate is used to verify that a closed channel update is of the @@ -756,7 +733,7 @@ func testFundingExpiryBlocksOnPending(ht *lntest.HarnessTest) { ht.EnsureConnected(alice, bob) param := lntest.OpenChannelParams{Amt: 100000} - update := ht.OpenChannelAssertPending(alice, bob, param) + ht.OpenChannelAssertPending(alice, bob, param) // At this point, the channel's funding transaction will have been // broadcast, but not confirmed. Alice and Bob's nodes should reflect @@ -777,20 +754,6 @@ func testFundingExpiryBlocksOnPending(ht *lntest.HarnessTest) { // Mine 1 block to confirm the funding transaction, and then close the // channel. ht.MineBlocksAndAssertNumTxes(1, 1) - chanPoint := lntest.ChanPointFromPendingUpdate(update) - - // TODO(yy): remove the sleep once the following bug is fixed. - // - // We may get the error `unable to gracefully close channel - // while peer is offline (try force closing it instead): - // channel link not found`. This happens because the channel - // link hasn't been added yet but we now proceed to closing the - // channel. We may need to revisit how the channel open event - // is created and make sure the event is only sent after all - // relevant states have been updated. - time.Sleep(2 * time.Second) - - ht.CloseChannel(alice, chanPoint) } // testSimpleTaprootChannelActivation ensures that a simple taproot channel is @@ -803,9 +766,7 @@ func testSimpleTaprootChannelActivation(ht *lntest.HarnessTest) { // Make the new set of participants. alice := ht.NewNode("alice", simpleTaprootChanArgs) - defer ht.Shutdown(alice) bob := ht.NewNode("bob", simpleTaprootChanArgs) - defer ht.Shutdown(bob) ht.FundCoins(btcutil.SatoshiPerBitcoin, alice) @@ -842,9 +803,6 @@ func testSimpleTaprootChannelActivation(ht *lntest.HarnessTest) { // Verify that Alice sees an active channel to Bob. ht.AssertChannelActive(alice, chanPoint) - - // Our test is done and Alice closes her channel to Bob. - ht.CloseChannel(alice, chanPoint) } // testOpenChannelLockedBalance tests that when a funding reservation is diff --git a/itest/lnd_payment_test.go b/itest/lnd_payment_test.go index 6c6fc1b178..d36be796aa 100644 --- a/itest/lnd_payment_test.go +++ b/itest/lnd_payment_test.go @@ -477,9 +477,7 @@ func testListPayments(ht *lntest.HarnessTest) { // Open a channel with 100k satoshis between Alice and Bob with Alice // being the sole funder of the channel. chanAmt := btcutil.Amount(100000) - chanPoint := ht.OpenChannel( - alice, bob, lntest.OpenChannelParams{Amt: chanAmt}, - ) + ht.OpenChannel(alice, bob, lntest.OpenChannelParams{Amt: chanAmt}) // Now that the channel is open, create an invoice for Bob which // expects a payment of 1000 satoshis from Alice paid via a particular @@ -638,17 +636,6 @@ func testListPayments(ht *lntest.HarnessTest) { // Check that there are no payments after test. ht.AssertNumPayments(alice, 0) - - // TODO(yy): remove the sleep once the following bug is fixed. - // When the invoice is reported settled, the commitment dance is not - // yet finished, which can cause an error when closing the channel, - // saying there's active HTLCs. We need to investigate this issue and - // reverse the order to, first finish the commitment dance, then report - // the invoice as settled. - time.Sleep(2 * time.Second) - - // Close the channel. - ht.CloseChannel(alice, chanPoint) } // testPaymentFollowingChannelOpen tests that the channel transition from @@ -698,19 +685,6 @@ func testPaymentFollowingChannelOpen(ht *lntest.HarnessTest) { // Send payment to Bob so that a channel update to disk will be // executed. ht.CompletePaymentRequests(alice, []string{bobPayReqs[0]}) - - // TODO(yy): remove the sleep once the following bug is fixed. - // When the invoice is reported settled, the commitment dance is not - // yet finished, which can cause an error when closing the channel, - // saying there's active HTLCs. We need to investigate this issue and - // reverse the order to, first finish the commitment dance, then report - // the invoice as settled. - time.Sleep(2 * time.Second) - - // Finally, immediately close the channel. This function will also - // block until the channel is closed and will additionally assert the - // relevant channel closing post conditions. - ht.CloseChannel(alice, chanPoint) } // testAsyncPayments tests the performance of the async payments. @@ -818,11 +792,6 @@ func runAsyncPayments(ht *lntest.HarnessTest, alice, bob *node.HarnessNode, ht.Log("\tBenchmark info: Elapsed time: ", timeTaken) ht.Log("\tBenchmark info: TPS: ", float64(numInvoices)/timeTaken.Seconds()) - - // Finally, immediately close the channel. This function will also - // block until the channel is closed and will additionally assert the - // relevant channel closing post conditions. - ht.CloseChannel(alice, chanPoint) } // testBidirectionalAsyncPayments tests that nodes are able to send the @@ -930,11 +899,6 @@ func testBidirectionalAsyncPayments(ht *lntest.HarnessTest) { // Next query for Bob's and Alice's channel states, in order to confirm // that all payment have been successfully transmitted. assertChannelState(ht, bob, chanPoint, bobAmt, aliceAmt) - - // Finally, immediately close the channel. This function will also - // block until the channel is closed and will additionally assert the - // relevant channel closing post conditions. - ht.CloseChannel(alice, chanPoint) } func testInvoiceSubscriptions(ht *lntest.HarnessTest) { @@ -951,9 +915,7 @@ func testInvoiceSubscriptions(ht *lntest.HarnessTest) { // Open a channel with 500k satoshis between Alice and Bob with Alice // being the sole funder of the channel. - chanPoint := ht.OpenChannel( - alice, bob, lntest.OpenChannelParams{Amt: chanAmt}, - ) + ht.OpenChannel(alice, bob, lntest.OpenChannelParams{Amt: chanAmt}) // Next create a new invoice for Bob requesting 1k satoshis. const paymentAmt = 1000 @@ -1055,16 +1017,6 @@ func testInvoiceSubscriptions(ht *lntest.HarnessTest) { // At this point, all the invoices should be fully settled. require.Empty(ht, settledInvoices, "not all invoices settled") - - // TODO(yy): remove the sleep once the following bug is fixed. - // When the invoice is reported settled, the commitment dance is not - // yet finished, which can cause an error when closing the channel, - // saying there's active HTLCs. We need to investigate this issue and - // reverse the order to, first finish the commitment dance, then report - // the invoice as settled. - time.Sleep(2 * time.Second) - - ht.CloseChannel(alice, chanPoint) } // assertChannelState asserts the channel state by checking the values in @@ -1151,10 +1103,6 @@ func testPaymentFailureReasonCanceled(ht *lntest.HarnessTest) { ht, ts, cpAB, routerrpc.ResolveHoldForwardAction_FAIL, lnrpc.Payment_FAILED, interceptor, ) - - // Finally, close channels. - ht.CloseChannel(alice, cpAB) - ht.CloseChannel(bob, cpBC) } func sendPaymentInterceptAndCancel(ht *lntest.HarnessTest, diff --git a/itest/lnd_psbt_test.go b/itest/lnd_psbt_test.go index f3687d74b5..1bba998be1 100644 --- a/itest/lnd_psbt_test.go +++ b/itest/lnd_psbt_test.go @@ -324,13 +324,6 @@ func runPsbtChanFunding(ht *lntest.HarnessTest, carol, dave *node.HarnessNode, } resp := dave.RPC.AddInvoice(invoice) ht.CompletePaymentRequests(carol, []string{resp.PaymentRequest}) - - // To conclude, we'll close the newly created channel between Carol and - // Dave. This function will also block until the channel is closed and - // will additionally assert the relevant channel closing post - // conditions. - ht.CloseChannel(carol, chanPoint) - ht.CloseChannel(carol, chanPoint2) } // runPsbtChanFundingExternal makes sure a channel can be opened between carol @@ -499,13 +492,6 @@ func runPsbtChanFundingExternal(ht *lntest.HarnessTest, carol, } resp := dave.RPC.AddInvoice(invoice) ht.CompletePaymentRequests(carol, []string{resp.PaymentRequest}) - - // To conclude, we'll close the newly created channel between Carol and - // Dave. This function will also block until the channels are closed and - // will additionally assert the relevant channel closing post - // conditions. - ht.CloseChannel(carol, chanPoint) - ht.CloseChannel(carol, chanPoint2) } // runPsbtChanFundingSingleStep checks whether PSBT funding works also when @@ -649,12 +635,6 @@ func runPsbtChanFundingSingleStep(ht *lntest.HarnessTest, carol, } resp := dave.RPC.AddInvoice(invoice) ht.CompletePaymentRequests(carol, []string{resp.PaymentRequest}) - - // To conclude, we'll close the newly created channel between Carol and - // Dave. This function will also block until the channel is closed and - // will additionally assert the relevant channel closing post - // conditions. - ht.CloseChannel(carol, chanPoint) } // testSignPsbt tests that the SignPsbt RPC works correctly. diff --git a/itest/lnd_res_handoff_test.go b/itest/lnd_res_handoff_test.go index 7238bf6b76..d7bf499905 100644 --- a/itest/lnd_res_handoff_test.go +++ b/itest/lnd_res_handoff_test.go @@ -94,6 +94,4 @@ func testResHandoff(ht *lntest.HarnessTest) { // Assert that Alice's payment failed. ht.AssertFirstHTLCError(alice, lnrpc.Failure_PERMANENT_CHANNEL_FAILURE) - - ht.CloseChannel(alice, chanPointAlice) } diff --git a/itest/lnd_rest_api_test.go b/itest/lnd_rest_api_test.go index e559230d9a..7c4a3d988e 100644 --- a/itest/lnd_rest_api_test.go +++ b/itest/lnd_rest_api_test.go @@ -529,10 +529,9 @@ func wsTestCaseBiDirectionalSubscription(ht *lntest.HarnessTest) { // sent over the web socket. const numChannels = 3 for i := 0; i < numChannels; i++ { - chanPoint := ht.OpenChannel( + ht.OpenChannel( bob, alice, lntest.OpenChannelParams{Amt: 500000}, ) - defer ht.CloseChannel(bob, chanPoint) select { case <-msgChan: diff --git a/itest/lnd_route_blinding_test.go b/itest/lnd_route_blinding_test.go index c1811c4665..189130a89b 100644 --- a/itest/lnd_route_blinding_test.go +++ b/itest/lnd_route_blinding_test.go @@ -309,9 +309,6 @@ func testQueryBlindedRoutes(ht *lntest.HarnessTest) { require.Len(ht, resp.Routes, 1) require.Len(ht, resp.Routes[0].Hops, 2) require.Equal(ht, resp.Routes[0].TotalTimeLock, sendToIntroTimelock) - - ht.CloseChannel(alice, chanPointAliceBob) - ht.CloseChannel(bob, chanPointBobCarol) } type blindedForwardTest struct { @@ -410,10 +407,6 @@ func (b *blindedForwardTest) buildBlindedPath() *lnrpc.BlindedPaymentPath { // cleanup tears down all channels created by the test and cancels the top // level context used in the test. func (b *blindedForwardTest) cleanup() { - b.ht.CloseChannel(b.alice, b.channels[0]) - b.ht.CloseChannel(b.bob, b.channels[1]) - b.ht.CloseChannel(b.carol, b.channels[2]) - b.cancel() } @@ -844,8 +837,6 @@ func testErrorHandlingOnChainFailure(ht *lntest.HarnessTest) { // Manually close out the rest of our channels and cancel (don't use // built in cleanup which will try close the already-force-closed // channel). - ht.CloseChannel(alice, testCase.channels[0]) - ht.CloseChannel(testCase.carol, testCase.channels[2]) testCase.cancel() } diff --git a/itest/lnd_routing_test.go b/itest/lnd_routing_test.go index e05bf441b2..63c5d54189 100644 --- a/itest/lnd_routing_test.go +++ b/itest/lnd_routing_test.go @@ -101,7 +101,6 @@ func testSingleHopSendToRouteCase(ht *lntest.HarnessTest, chanPointCarol := ht.OpenChannel( carol, dave, lntest.OpenChannelParams{Amt: chanAmt}, ) - defer ht.CloseChannel(carol, chanPointCarol) // Create invoices for Dave, which expect a payment from Carol. payReqs, rHashes, _ := ht.CreatePayReqs( @@ -328,7 +327,6 @@ func runMultiHopSendToRoute(ht *lntest.HarnessTest, useGraphCache bool) { chanPointAlice := ht.OpenChannel( alice, bob, lntest.OpenChannelParams{Amt: chanAmt}, ) - defer ht.CloseChannel(alice, chanPointAlice) // Create Carol and establish a channel from Bob. Bob is the sole // funder of the channel with 100k satoshis. The network topology @@ -340,7 +338,6 @@ func runMultiHopSendToRoute(ht *lntest.HarnessTest, useGraphCache bool) { chanPointBob := ht.OpenChannel( bob, carol, lntest.OpenChannelParams{Amt: chanAmt}, ) - defer ht.CloseChannel(carol, chanPointBob) // Make sure Alice knows the channel between Bob and Carol. ht.AssertChannelInGraph(alice, chanPointBob) @@ -420,9 +417,7 @@ func testSendToRouteErrorPropagation(ht *lntest.HarnessTest) { bob := ht.NewNode("Bob", nil) ht.EnsureConnected(alice, bob) - chanPointAlice := ht.OpenChannel( - alice, bob, lntest.OpenChannelParams{Amt: chanAmt}, - ) + ht.OpenChannel(alice, bob, lntest.OpenChannelParams{Amt: chanAmt}) // Create a new nodes (Carol and Charlie), load her with some funds, // then establish a connection between Carol and Charlie with a channel @@ -476,8 +471,6 @@ func testSendToRouteErrorPropagation(ht *lntest.HarnessTest) { require.NoError(ht, err, "payment stream has been closed but fake "+ "route has consumed") require.Contains(ht, event.PaymentError, "UnknownNextPeer") - - ht.CloseChannel(alice, chanPointAlice) } // testPrivateChannels tests that a private channel can be used for @@ -602,12 +595,6 @@ func testPrivateChannels(ht *lntest.HarnessTest) { ht.AssertNumActiveEdges(carol, 4, true) ht.AssertNumActiveEdges(carol, 3, false) ht.AssertNumActiveEdges(dave, 3, true) - - // Close all channels. - ht.CloseChannel(alice, chanPointAlice) - ht.CloseChannel(dave, chanPointDave) - ht.CloseChannel(carol, chanPointCarol) - ht.CloseChannel(carol, chanPointPrivate) } // testInvoiceRoutingHints tests that the routing hints for an invoice are @@ -641,7 +628,7 @@ func testInvoiceRoutingHints(ht *lntest.HarnessTest) { carol := ht.NewNode("Carol", nil) ht.ConnectNodes(alice, carol) - chanPointCarol := ht.OpenChannel( + ht.OpenChannel( alice, carol, lntest.OpenChannelParams{ Amt: chanAmt, PushAmt: chanAmt / 2, @@ -654,7 +641,7 @@ func testInvoiceRoutingHints(ht *lntest.HarnessTest) { // advertised, otherwise we'd end up leaking information about nodes // that wish to stay unadvertised. ht.ConnectNodes(bob, carol) - chanPointBobCarol := ht.OpenChannel( + ht.OpenChannel( bob, carol, lntest.OpenChannelParams{ Amt: chanAmt, PushAmt: chanAmt / 2, @@ -668,7 +655,7 @@ func testInvoiceRoutingHints(ht *lntest.HarnessTest) { dave := ht.NewNode("Dave", nil) ht.ConnectNodes(alice, dave) - chanPointDave := ht.OpenChannel( + ht.OpenChannel( alice, dave, lntest.OpenChannelParams{ Amt: chanAmt, Private: true, @@ -681,7 +668,7 @@ func testInvoiceRoutingHints(ht *lntest.HarnessTest) { // inactive channels. eve := ht.NewNode("Eve", nil) ht.ConnectNodes(alice, eve) - chanPointEve := ht.OpenChannel( + ht.OpenChannel( alice, eve, lntest.OpenChannelParams{ Amt: chanAmt, PushAmt: chanAmt / 2, @@ -742,17 +729,6 @@ func testInvoiceRoutingHints(ht *lntest.HarnessTest) { Private: true, } checkInvoiceHints(invoice) - - // Now that we've confirmed the routing hints were added correctly, we - // can close all the channels and shut down all the nodes created. - ht.CloseChannel(alice, chanPointBob) - ht.CloseChannel(alice, chanPointCarol) - ht.CloseChannel(bob, chanPointBobCarol) - ht.CloseChannel(alice, chanPointDave) - - // The channel between Alice and Eve should be force closed since Eve - // is offline. - ht.ForceCloseChannel(alice, chanPointEve) } // testScidAliasRoutingHints tests that dynamically created aliases via the RPC @@ -877,7 +853,7 @@ func testScidAliasRoutingHints(ht *lntest.HarnessTest) { // Connect the existing Bob node with Carol via a public channel. ht.ConnectNodes(bob, carol) - chanPointBC := ht.OpenChannel(bob, carol, lntest.OpenChannelParams{ + ht.OpenChannel(bob, carol, lntest.OpenChannelParams{ Amt: chanAmt, PushAmt: chanAmt / 2, }) @@ -940,9 +916,6 @@ func testScidAliasRoutingHints(ht *lntest.HarnessTest) { FeeLimitSat: math.MaxInt64, }) ht.AssertPaymentStatusFromStream(stream2, lnrpc.Payment_FAILED) - - ht.CloseChannel(carol, chanPointCD) - ht.CloseChannel(bob, chanPointBC) } // testMultiHopOverPrivateChannels tests that private channels can be used as @@ -1042,12 +1015,6 @@ func testMultiHopOverPrivateChannels(ht *lntest.HarnessTest) { // Alice should have sent 20k satoshis + fee for two hops to Bob. ht.AssertAmountPaid("Alice(local) [private=>] Bob(remote)", alice, chanPointAlice, paymentAmt+baseFee*2, 0) - - // At this point, the payment was successful. We can now close all the - // channels and shutdown the nodes created throughout this test. - ht.CloseChannel(alice, chanPointAlice) - ht.CloseChannel(bob, chanPointBob) - ht.CloseChannel(carol, chanPointCarol) } // testQueryRoutes checks the response of queryroutes. @@ -1085,7 +1052,6 @@ func testQueryRoutes(ht *lntest.HarnessTest) { resp := ht.OpenMultiChannelsAsync(reqs) // Extract channel points from the response. - chanPointAlice := resp[0] chanPointBob := resp[1] chanPointCarol := resp[2] @@ -1196,12 +1162,6 @@ func testQueryRoutes(ht *lntest.HarnessTest) { // control import function updates appropriately. testMissionControlCfg(ht.T, alice) testMissionControlImport(ht, alice, bob.PubKey[:], carol.PubKey[:]) - - // We clean up the test case by closing channels that were created for - // the duration of the tests. - ht.CloseChannel(alice, chanPointAlice) - ht.CloseChannel(bob, chanPointBob) - ht.CloseChannel(carol, chanPointCarol) } // testMissionControlCfg tests getting and setting of a node's mission control @@ -1528,13 +1488,6 @@ func testRouteFeeCutoff(ht *lntest.HarnessTest) { }, } testFeeCutoff(feeLimitFixed) - - // Once we're done, close the channels and shut down the nodes created - // throughout this test. - ht.CloseChannel(alice, chanPointAliceBob) - ht.CloseChannel(alice, chanPointAliceCarol) - ht.CloseChannel(bob, chanPointBobDave) - ht.CloseChannel(carol, chanPointCarolDave) } // testFeeLimitAfterQueryRoutes tests that a payment's fee limit is consistent @@ -1547,7 +1500,7 @@ func testFeeLimitAfterQueryRoutes(ht *lntest.HarnessTest) { cfgs, lntest.OpenChannelParams{Amt: chanAmt}, ) alice, bob, carol := nodes[0], nodes[1], nodes[2] - chanPointAliceBob, chanPointBobCarol := chanPoints[0], chanPoints[1] + chanPointAliceBob := chanPoints[0] // We set an inbound fee discount on Bob's channel to Alice to // effectively set the outbound fees charged to Carol to zero. @@ -1606,10 +1559,6 @@ func testFeeLimitAfterQueryRoutes(ht *lntest.HarnessTest) { // We assert that a route compatible with the fee limit is available. ht.SendPaymentAssertSettled(alice, sendReq) - - // Once we're done, close the channels. - ht.CloseChannel(alice, chanPointAliceBob) - ht.CloseChannel(bob, chanPointBobCarol) } // computeFee calculates the payment fee as specified in BOLT07. diff --git a/itest/lnd_single_hop_invoice_test.go b/itest/lnd_single_hop_invoice_test.go index 4adc37b72e..954f8666ef 100644 --- a/itest/lnd_single_hop_invoice_test.go +++ b/itest/lnd_single_hop_invoice_test.go @@ -137,6 +137,4 @@ func testSingleHopInvoice(ht *lntest.HarnessTest) { require.EqualValues(ht, 1, hopHint.FeeBaseMsat, "wrong FeeBaseMsat") require.EqualValues(ht, 20, hopHint.CltvExpiryDelta, "wrong CltvExpiryDelta") - - ht.CloseChannel(alice, cp) } diff --git a/itest/lnd_switch_test.go b/itest/lnd_switch_test.go index ca68802fe0..59e8ce9cec 100644 --- a/itest/lnd_switch_test.go +++ b/itest/lnd_switch_test.go @@ -29,7 +29,6 @@ func testSwitchCircuitPersistence(ht *lntest.HarnessTest) { // Setup our test scenario. We should now have four nodes running with // three channels. s := setupScenarioFourNodes(ht) - defer s.cleanUp() // Restart the intermediaries and the sender. ht.RestartNode(s.dave) @@ -99,7 +98,6 @@ func testSwitchOfflineDelivery(ht *lntest.HarnessTest) { // Setup our test scenario. We should now have four nodes running with // three channels. s := setupScenarioFourNodes(ht) - defer s.cleanUp() // First, disconnect Dave and Alice so that their link is broken. ht.DisconnectNodes(s.dave, s.alice) @@ -175,7 +173,6 @@ func testSwitchOfflineDeliveryPersistence(ht *lntest.HarnessTest) { // Setup our test scenario. We should now have four nodes running with // three channels. s := setupScenarioFourNodes(ht) - defer s.cleanUp() // Disconnect the two intermediaries, Alice and Dave, by shutting down // Alice. @@ -264,7 +261,6 @@ func testSwitchOfflineDeliveryOutgoingOffline(ht *lntest.HarnessTest) { // three channels. Note that we won't call the cleanUp function here as // we will manually stop the node Carol and her channel. s := setupScenarioFourNodes(ht) - defer s.cleanUp() // Disconnect the two intermediaries, Alice and Dave, so that when carol // restarts, the response will be held by Dave. @@ -355,8 +351,6 @@ type scenarioFourNodes struct { chanPointAliceBob *lnrpc.ChannelPoint chanPointCarolDave *lnrpc.ChannelPoint chanPointDaveAlice *lnrpc.ChannelPoint - - cleanUp func() } // setupScenarioFourNodes creates a topology for switch tests. It will create @@ -433,21 +427,9 @@ func setupScenarioFourNodes(ht *lntest.HarnessTest) *scenarioFourNodes { // above. ht.CompletePaymentRequestsNoWait(bob, payReqs, chanPointAliceBob) - // Create a cleanUp to wipe the states. - cleanUp := func() { - if ht.Failed() { - ht.Skip("Skipped cleanup for failed test") - return - } - - ht.CloseChannel(alice, chanPointAliceBob) - ht.CloseChannel(dave, chanPointDaveAlice) - ht.CloseChannel(carol, chanPointCarolDave) - } - s := &scenarioFourNodes{ alice, bob, carol, dave, chanPointAliceBob, - chanPointCarolDave, chanPointDaveAlice, cleanUp, + chanPointCarolDave, chanPointDaveAlice, } // Wait until all nodes in the network have 5 outstanding htlcs. diff --git a/itest/lnd_trackpayments_test.go b/itest/lnd_trackpayments_test.go index 0dd3467155..29fb2c0b3a 100644 --- a/itest/lnd_trackpayments_test.go +++ b/itest/lnd_trackpayments_test.go @@ -2,7 +2,6 @@ package itest import ( "encoding/hex" - "time" "github.com/btcsuite/btcd/btcutil" "github.com/lightningnetwork/lnd/lnrpc" @@ -20,12 +19,11 @@ func testTrackPayments(ht *lntest.HarnessTest) { cfgs := [][]string{cfgAlice, nil} // Create a channel Alice->Bob. - chanPoints, nodes := ht.CreateSimpleNetwork( + _, nodes := ht.CreateSimpleNetwork( cfgs, lntest.OpenChannelParams{ Amt: btcutil.Amount(300000), }, ) - channel := chanPoints[0] alice, bob := nodes[0], nodes[1] // Call the TrackPayments api to listen for payment updates. @@ -86,28 +84,17 @@ func testTrackPayments(ht *lntest.HarnessTest) { require.Equal(ht, amountMsat, update3.ValueMsat) require.Equal(ht, hex.EncodeToString(invoice.RPreimage), update3.PaymentPreimage) - - // TODO(yy): remove the sleep once the following bug is fixed. - // When the invoice is reported settled, the commitment dance is not - // yet finished, which can cause an error when closing the channel, - // saying there's active HTLCs. We need to investigate this issue and - // reverse the order to, first finish the commitment dance, then report - // the invoice as settled. - time.Sleep(2 * time.Second) - - ht.CloseChannel(alice, channel) } // testTrackPaymentsCompatible checks that when `routerrpc.usestatusinitiated` // is not set, the new Payment_INITIATED is replaced with Payment_IN_FLIGHT. func testTrackPaymentsCompatible(ht *lntest.HarnessTest) { // Open a channel between alice and bob. - chanPoints, nodes := ht.CreateSimpleNetwork( + _, nodes := ht.CreateSimpleNetwork( [][]string{nil, nil}, lntest.OpenChannelParams{ Amt: btcutil.Amount(300000), }, ) - channel := chanPoints[0] alice, bob := nodes[0], nodes[1] // Call the TrackPayments api to listen for payment updates. @@ -162,14 +149,4 @@ func testTrackPaymentsCompatible(ht *lntest.HarnessTest) { payment3, err := paymentClient.Recv() require.NoError(ht, err, "unable to get payment update") require.Equal(ht, lnrpc.Payment_SUCCEEDED, payment3.Status) - - // TODO(yy): remove the sleep once the following bug is fixed. - // When the invoice is reported settled, the commitment dance is not - // yet finished, which can cause an error when closing the channel, - // saying there's active HTLCs. We need to investigate this issue and - // reverse the order to, first finish the commitment dance, then report - // the invoice as settled. - time.Sleep(2 * time.Second) - - ht.CloseChannel(alice, channel) } diff --git a/itest/lnd_wumbo_channels_test.go b/itest/lnd_wumbo_channels_test.go index 18d170acc3..8bf18106e7 100644 --- a/itest/lnd_wumbo_channels_test.go +++ b/itest/lnd_wumbo_channels_test.go @@ -45,8 +45,7 @@ func testWumboChannels(ht *lntest.HarnessTest) { // Creating a wumbo channel between these two nodes should succeed. ht.EnsureConnected(wumboNode, wumboNode2) - chanPoint := ht.OpenChannel( + ht.OpenChannel( wumboNode, wumboNode2, lntest.OpenChannelParams{Amt: chanAmt}, ) - ht.CloseChannel(wumboNode, chanPoint) } diff --git a/itest/lnd_zero_conf_test.go b/itest/lnd_zero_conf_test.go index 523a41c373..8ec19945ca 100644 --- a/itest/lnd_zero_conf_test.go +++ b/itest/lnd_zero_conf_test.go @@ -854,9 +854,6 @@ func testOptionScidUpgrade(ht *lntest.HarnessTest) { daveInvoice2 := dave.RPC.AddInvoice(daveParams) ht.CompletePaymentRequests(bob, []string{daveInvoice2.PaymentRequest}) - - // Close standby node's channels. - ht.CloseChannel(bob, fundingPoint2) } // acceptChannel is used to accept a single channel that comes across. This From 822b71abf0c8e33a0d551bfe3694366873bb1ec1 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 2 Nov 2024 18:36:45 +0800 Subject: [PATCH 101/153] lntest: make sure node is properly shut down Soemtimes the node may be hanging for a while without being noticed, causing failures in its following tests, thus making the debugging extrememly difficult. We now assert the node has been shut down from the logs to assert the shutdown process behaves as expected. --- lntest/node/harness_node.go | 61 +++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/lntest/node/harness_node.go b/lntest/node/harness_node.go index 9ef1f212c0..d1353c25cf 100644 --- a/lntest/node/harness_node.go +++ b/lntest/node/harness_node.go @@ -983,6 +983,67 @@ func finalizeLogfile(hn *HarnessNode) { getFinalizedLogFilePrefix(hn), ) renameFile(hn.filename, newFileName) + + // Assert the node has shut down from the log file. + err := assertNodeShutdown(newFileName) + if err != nil { + err := fmt.Errorf("[%s]: assert shutdown failed in log[%s]: %w", + hn.Name(), newFileName, err) + panic(err) + } +} + +// assertNodeShutdown asserts that the node has shut down properly by checking +// the last lines of the log file for the shutdown message "Shutdown complete". +func assertNodeShutdown(filename string) error { + file, err := os.Open(filename) + if err != nil { + return err + } + defer file.Close() + + // Read more than one line to make sure we get the last line. + // const linesSize = 200 + // + // NOTE: Reading 200 bytes of lines should be more than enough to find + // the `Shutdown complete` message. However, this is only true if the + // message is printed the last, which means `lnd` will properly wait + // for all its subsystems to shut down before exiting. Unfortunately + // there is at least one bug in the shutdown process where we don't + // wait for the chain backend to fully quit first, which can be easily + // reproduced by turning on `RPCC=trace` and use a linesSize of 200. + // + // TODO(yy): fix the shutdown process and remove this workaround by + // refactoring the lnd to use only one rpcclient, which requires quite + // some work on the btcwallet front. + const linesSize = 1000 + + buf := make([]byte, linesSize) + stat, statErr := file.Stat() + if statErr != nil { + return err + } + + start := stat.Size() - linesSize + _, err = file.ReadAt(buf, start) + if err != nil { + return err + } + + // Exit early if the shutdown line is found. + if bytes.Contains(buf, []byte("Shutdown complete")) { + return nil + } + + // For etcd tests, we need to check for the line where the node is + // blocked at wallet unlock since we are testing how such a behavior is + // handled by etcd. + if bytes.Contains(buf, []byte("wallet and unlock")) { + return nil + } + + return fmt.Errorf("node did not shut down properly: found log "+ + "lines: %s", buf) } // finalizeEtcdLog saves the etcd log files when test ends. From 0019cb37b28842c3a6c2e6e02cc7af09474f49ff Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Wed, 20 Nov 2024 14:52:28 +0800 Subject: [PATCH 102/153] lntest: add human-readble names and check num of nodes --- lntest/harness.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lntest/harness.go b/lntest/harness.go index 5b146a6eca..1dac9a12d8 100644 --- a/lntest/harness.go +++ b/lntest/harness.go @@ -2165,6 +2165,13 @@ func acceptChannel(t *testing.T, zeroConf bool, stream rpc.AcceptorClient) { require.NoError(t, err) } +// nodeNames defines a slice of human-reable names for the nodes created in the +// `createNodes` method. 8 nodes are defined here as by default we can only +// create this many nodes in one test. +var nodeNames = []string{ + "Alice", "Bob", "Carol", "Dave", "Eve", "Frank", "Grace", "Heidi", +} + // createNodes creates the number of nodes specified by the number of configs. // Each node is created using the specified config, the neighbors are // connected. @@ -2172,12 +2179,15 @@ func (h *HarnessTest) createNodes(nodeCfgs [][]string) []*node.HarnessNode { // Get the number of nodes. numNodes := len(nodeCfgs) + // Make sure we are creating a reasonable number of nodes. + require.LessOrEqual(h, numNodes, len(nodeNames), "too many nodes") + // Make a slice of nodes. nodes := make([]*node.HarnessNode, numNodes) // Create new nodes. for i, nodeCfg := range nodeCfgs { - nodeName := fmt.Sprintf("Node%q", string(rune('A'+i))) + nodeName := nodeNames[i] n := h.NewNode(nodeName, nodeCfg) nodes[i] = n } From 1ffe9215dab456c45661911bc3eaca00fe3e3a98 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 29 Oct 2024 16:00:04 +0800 Subject: [PATCH 103/153] itest: fix `testOpenChannelUpdateFeePolicy` This commit fixes a misuse of `ht.Subtest`, where the nodes should always be created inside the subtest. --- itest/lnd_open_channel_test.go | 34 ++++++++++++---------------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/itest/lnd_open_channel_test.go b/itest/lnd_open_channel_test.go index 9fb41a2dea..13a581921c 100644 --- a/itest/lnd_open_channel_test.go +++ b/itest/lnd_open_channel_test.go @@ -238,19 +238,22 @@ func testOpenChannelUpdateFeePolicy(ht *lntest.HarnessTest) { MaxHtlcMsat: defaultMaxHtlc, } - // In this basic test, we'll need a third node, Carol, so we can forward - // a payment through the channel we'll open with the different fee - // policies. - alice := ht.NewNodeWithCoins("Alice", nil) - bob := ht.NewNode("Bob", nil) - carol := ht.NewNode("Carol", nil) - - nodes := []*node.HarnessNode{alice, bob, carol} - runTestCase := func(ht *lntest.HarnessTest, chanParams lntest.OpenChannelParams, alicePolicy, bobPolicy *lnrpc.RoutingPolicy) { + // In this basic test, we'll need a third node, Carol, so we + // can forward a payment through the channel we'll open with + // the different fee policies. + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNode("Bob", nil) + carol := ht.NewNodeWithCoins("Carol", nil) + + ht.EnsureConnected(alice, bob) + ht.EnsureConnected(alice, carol) + + nodes := []*node.HarnessNode{alice, bob, carol} + // Create a channel Alice->Bob. chanPoint := ht.OpenChannel(alice, bob, chanParams) @@ -287,19 +290,6 @@ func testOpenChannelUpdateFeePolicy(ht *lntest.HarnessTest) { for i, feeScenario := range feeScenarios { ht.Run(fmt.Sprintf("%d", i), func(t *testing.T) { st := ht.Subtest(t) - st.EnsureConnected(alice, bob) - - st.RestartNode(carol) - - // Because we're using ht.Subtest(), we need to restart - // any node we have to refresh its runtime context. - // Otherwise, we'll get a "context canceled" error on - // RPC calls. - st.EnsureConnected(alice, carol) - - // Send Carol enough coins to be able to open a channel - // to Alice. - st.FundCoins(btcutil.SatoshiPerBitcoin, carol) runTestCase( st, feeScenario, From c6819c02b72e5fc4ff47c8cef2b33f36ec0ad081 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Wed, 20 Nov 2024 14:48:16 +0800 Subject: [PATCH 104/153] itest: fix flake in `testSendDirectPayment` This bug was hidden because we used standby nodes before, which always have more-than-necessary wallet utxos. --- itest/lnd_payment_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/itest/lnd_payment_test.go b/itest/lnd_payment_test.go index d36be796aa..74adc4ffad 100644 --- a/itest/lnd_payment_test.go +++ b/itest/lnd_payment_test.go @@ -449,6 +449,12 @@ func testSendDirectPayment(ht *lntest.HarnessTest) { // Make sure they are connected. st.EnsureConnected(alice, bob) + // There's a bug that causes the funding to be failed + // due to the `ListCoins` cannot find the utxos. + // + // TODO(yy): remove this line to fix the ListCoins bug. + st.FundCoins(btcutil.SatoshiPerBitcoin, alice) + // Open a channel with 100k satoshis between Alice and // Bob with Alice being the sole funder of the channel. params := lntest.OpenChannelParams{ From 50e2495c4390758bf4e2c20f73d326138df4a4d3 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 29 Oct 2024 16:05:06 +0800 Subject: [PATCH 105/153] itest: fix spawning temp miner --- itest/lnd_open_channel_test.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/itest/lnd_open_channel_test.go b/itest/lnd_open_channel_test.go index 13a581921c..66930a6c24 100644 --- a/itest/lnd_open_channel_test.go +++ b/itest/lnd_open_channel_test.go @@ -29,14 +29,17 @@ func testOpenChannelAfterReorg(ht *lntest.HarnessTest) { ht.Skipf("skipping reorg test for neutrino backend") } - // Create a temp miner. - tempMiner := ht.SpawnTempMiner() - miner := ht.Miner() alice := ht.NewNodeWithCoins("Alice", nil) bob := ht.NewNode("Bob", nil) ht.EnsureConnected(alice, bob) + // Create a temp miner after the creation of Alice. + // + // NOTE: this is needed since NewNodeWithCoins will mine a block and + // the temp miner needs to sync up. + tempMiner := ht.SpawnTempMiner() + // Create a new channel that requires 1 confs before it's considered // open, then broadcast the funding transaction params := lntest.OpenChannelParams{ From 71eab38fb9dd9c023f0defbe762a8c35a12ee628 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 31 Oct 2024 03:24:34 +0800 Subject: [PATCH 106/153] itest: fix flake for neutrino backend --- itest/lnd_channel_force_close_test.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/itest/lnd_channel_force_close_test.go b/itest/lnd_channel_force_close_test.go index fca3838898..691f867997 100644 --- a/itest/lnd_channel_force_close_test.go +++ b/itest/lnd_channel_force_close_test.go @@ -340,6 +340,22 @@ func runChannelForceClosureTest(ht *lntest.HarnessTest, "sweep transaction not spending from commit") } + // For neutrino backend, due to it has no mempool, we need to check the + // sweep tx has already been saved to db before restarting. This is due + // to the possible race, + // - the fee bumper returns a TxPublished event, which is received by + // the sweeper and the sweep tx is saved to db. + // - the sweeper receives a shutdown signal before it receives the + // above event. + // + // TODO(yy): fix the above race. + if ht.IsNeutrinoBackend() { + // 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) + } + // Restart Alice to ensure that she resumes watching the finalized // commitment sweep txid. ht.RestartNode(alice) From 494f1e58e0c7b9252cebc6d6dc20574655196060 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 31 Oct 2024 05:57:22 +0800 Subject: [PATCH 107/153] itest: flatten PSBT funding test cases So it's easier to get the logs and debug. --- itest/list_on_test.go | 5 +- itest/lnd_psbt_test.go | 202 +++++++++++++++----------------- itest/lnd_remote_signer_test.go | 2 +- 3 files changed, 94 insertions(+), 115 deletions(-) diff --git a/itest/list_on_test.go b/itest/list_on_test.go index b93807437e..ac1653bfc8 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -283,10 +283,6 @@ var allTestCases = []*lntest.TestCase{ Name: "open channel reorg test", TestFunc: testOpenChannelAfterReorg, }, - { - Name: "psbt channel funding", - TestFunc: testPsbtChanFunding, - }, { Name: "sign psbt", TestFunc: testSignPsbt, @@ -686,4 +682,5 @@ func init() { // Register subtests. allTestCases = append(allTestCases, multiHopForceCloseTestCases...) allTestCases = append(allTestCases, watchtowerTestCases...) + allTestCases = append(allTestCases, psbtFundingTestCases...) } diff --git a/itest/lnd_psbt_test.go b/itest/lnd_psbt_test.go index 1bba998be1..a91ddef7fb 100644 --- a/itest/lnd_psbt_test.go +++ b/itest/lnd_psbt_test.go @@ -4,7 +4,6 @@ import ( "bytes" "encoding/hex" "testing" - "time" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/ecdsa" @@ -27,119 +26,82 @@ import ( "github.com/stretchr/testify/require" ) -// testPsbtChanFunding makes sure a channel can be opened between carol and dave -// by using a Partially Signed Bitcoin Transaction that funds the channel -// multisig funding output. -func testPsbtChanFunding(ht *lntest.HarnessTest) { - const ( - burnAddr = "bcrt1qxsnqpdc842lu8c0xlllgvejt6rhy49u6fmpgyz" - ) - - testCases := []struct { - name string - commitmentType lnrpc.CommitmentType - private bool - }{ - { - name: "anchors", - commitmentType: lnrpc.CommitmentType_ANCHORS, - private: false, +// psbtFundingTestCases contains the test cases for funding via PSBT. +var psbtFundingTestCases = []*lntest.TestCase{ + { + Name: "psbt funding anchor", + TestFunc: func(ht *lntest.HarnessTest) { + runPsbtChanFunding( + ht, false, lnrpc.CommitmentType_ANCHORS, + ) }, - { - name: "simple taproot", - commitmentType: lnrpc.CommitmentType_SIMPLE_TAPROOT, - - // Set this to true once simple taproot channels can be - // announced to the network. - private: true, + }, + { + Name: "psbt external funding anchor", + TestFunc: func(ht *lntest.HarnessTest) { + runPsbtChanFundingExternal( + ht, false, lnrpc.CommitmentType_ANCHORS, + ) }, - } + }, + { + Name: "psbt single step funding anchor", + TestFunc: func(ht *lntest.HarnessTest) { + runPsbtChanFundingSingleStep( + ht, false, lnrpc.CommitmentType_ANCHORS, + ) + }, + }, + { + Name: "psbt funding simple taproot", + TestFunc: func(ht *lntest.HarnessTest) { + runPsbtChanFunding( + ht, true, lnrpc.CommitmentType_SIMPLE_TAPROOT, + ) + }, + }, + { + Name: "psbt external funding simple taproot", + TestFunc: func(ht *lntest.HarnessTest) { + runPsbtChanFundingExternal( + ht, true, lnrpc.CommitmentType_SIMPLE_TAPROOT, + ) + }, + }, + { + Name: "psbt single step funding simple taproot", + TestFunc: func(ht *lntest.HarnessTest) { + runPsbtChanFundingSingleStep( + ht, true, lnrpc.CommitmentType_SIMPLE_TAPROOT, + ) + }, + }, +} - for _, tc := range testCases { - tc := tc - - success := ht.T.Run(tc.name, func(tt *testing.T) { - st := ht.Subtest(tt) - - args := lntest.NodeArgsForCommitType(tc.commitmentType) - - // First, we'll create two new nodes that we'll use to - // open channels between for this test. Dave gets some - // coins that will be used to fund the PSBT, just to - // make sure that Carol has an empty wallet. - carol := st.NewNode("carol", args) - dave := st.NewNode("dave", args) - - // We just send enough funds to satisfy the anchor - // channel reserve for 5 channels (50k sats). - st.FundCoins(50_000, carol) - st.FundCoins(50_000, dave) - - st.RunTestCase(&lntest.TestCase{ - Name: tc.name, - TestFunc: func(sst *lntest.HarnessTest) { - runPsbtChanFunding( - sst, carol, dave, tc.private, - tc.commitmentType, - ) - }, - }) +// runPsbtChanFunding makes sure a channel can be opened between carol and dave +// by using a Partially Signed Bitcoin Transaction that funds the channel +// multisig funding output. +func runPsbtChanFunding(ht *lntest.HarnessTest, private bool, + commitType lnrpc.CommitmentType) { - // Empty out the wallets so there aren't any lingering - // coins. - sendAllCoinsConfirm(st, carol, burnAddr) - sendAllCoinsConfirm(st, dave, burnAddr) - - // Now we test the second scenario. Again, we just send - // enough funds to satisfy the anchor channel reserve - // for 5 channels (50k sats). - st.FundCoins(50_000, carol) - st.FundCoins(50_000, dave) - - st.RunTestCase(&lntest.TestCase{ - Name: tc.name, - TestFunc: func(sst *lntest.HarnessTest) { - runPsbtChanFundingExternal( - sst, carol, dave, tc.private, - tc.commitmentType, - ) - }, - }) + args := lntest.NodeArgsForCommitType(commitType) - // Empty out the wallets a last time, so there aren't - // any lingering coins. - sendAllCoinsConfirm(st, carol, burnAddr) - sendAllCoinsConfirm(st, dave, burnAddr) - - // The last test case tests the anchor channel reserve - // itself, so we need empty wallets. - st.RunTestCase(&lntest.TestCase{ - Name: tc.name, - TestFunc: func(sst *lntest.HarnessTest) { - runPsbtChanFundingSingleStep( - sst, carol, dave, tc.private, - tc.commitmentType, - ) - }, - }) - }) - if !success { - // Log failure time to help relate the lnd logs to the - // failure. - ht.Logf("Failure time: %v", time.Now().Format( - "2006-01-02 15:04:05.000", - )) + // First, we'll create two new nodes that we'll use to open channels + // between for this test. Dave gets some coins that will be used to + // fund the PSBT, just to make sure that Carol has an empty wallet. + carol := ht.NewNode("carol", args) + dave := ht.NewNode("dave", args) - break - } - } + // We just send enough funds to satisfy the anchor channel reserve for + // 5 channels (50k sats). + ht.FundCoins(50_000, carol) + ht.FundCoins(50_000, dave) + + runPsbtChanFundingWithNodes(ht, carol, dave, private, commitType) } -// runPsbtChanFunding makes sure a channel can be opened between carol and dave -// by using a Partially Signed Bitcoin Transaction that funds the channel -// multisig funding output. -func runPsbtChanFunding(ht *lntest.HarnessTest, carol, dave *node.HarnessNode, - private bool, commitType lnrpc.CommitmentType) { +func runPsbtChanFundingWithNodes(ht *lntest.HarnessTest, carol, + dave *node.HarnessNode, private bool, commitType lnrpc.CommitmentType) { const chanSize = funding.MaxBtcFundingAmount ht.FundCoins(btcutil.SatoshiPerBitcoin, dave) @@ -330,8 +292,21 @@ func runPsbtChanFunding(ht *lntest.HarnessTest, carol, dave *node.HarnessNode, // and dave by using a Partially Signed Bitcoin Transaction that funds the // channel multisig funding output and is fully funded by an external third // party. -func runPsbtChanFundingExternal(ht *lntest.HarnessTest, carol, - dave *node.HarnessNode, private bool, commitType lnrpc.CommitmentType) { +func runPsbtChanFundingExternal(ht *lntest.HarnessTest, private bool, + commitType lnrpc.CommitmentType) { + + args := lntest.NodeArgsForCommitType(commitType) + + // First, we'll create two new nodes that we'll use to open channels + // between for this test. Dave gets some coins that will be used to + // fund the PSBT, just to make sure that Carol has an empty wallet. + carol := ht.NewNode("carol", args) + dave := ht.NewNode("dave", args) + + // We just send enough funds to satisfy the anchor channel reserve for + // 5 channels (50k sats). + ht.FundCoins(50_000, carol) + ht.FundCoins(50_000, dave) const chanSize = funding.MaxBtcFundingAmount @@ -498,8 +473,15 @@ func runPsbtChanFundingExternal(ht *lntest.HarnessTest, carol, // the wallet of both nodes are empty and one of them uses PSBT and an external // wallet to fund the channel while creating reserve output in the same // transaction. -func runPsbtChanFundingSingleStep(ht *lntest.HarnessTest, carol, - dave *node.HarnessNode, private bool, commitType lnrpc.CommitmentType) { +func runPsbtChanFundingSingleStep(ht *lntest.HarnessTest, private bool, + commitType lnrpc.CommitmentType) { + + args := lntest.NodeArgsForCommitType(commitType) + + // First, we'll create two new nodes that we'll use to open channels + // between for this test. + carol := ht.NewNode("carol", args) + dave := ht.NewNode("dave", args) const chanSize = funding.MaxBtcFundingAmount diff --git a/itest/lnd_remote_signer_test.go b/itest/lnd_remote_signer_test.go index e18e5cb039..bd92ba37c8 100644 --- a/itest/lnd_remote_signer_test.go +++ b/itest/lnd_remote_signer_test.go @@ -123,7 +123,7 @@ func testRemoteSigner(ht *lntest.HarnessTest) { name: "psbt", randomSeed: true, fn: func(tt *lntest.HarnessTest, wo, carol *node.HarnessNode) { - runPsbtChanFunding( + runPsbtChanFundingWithNodes( tt, carol, wo, false, lnrpc.CommitmentType_LEGACY, ) From 8082b93d879288ec2aec1360c04e4c6b0687424e Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 31 Oct 2024 06:11:06 +0800 Subject: [PATCH 108/153] itest: fix and document flake in sweeping tests We previously didn't see this issue because we always have nodes being over-funded. --- itest/lnd_sweep_test.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/itest/lnd_sweep_test.go b/itest/lnd_sweep_test.go index 2a226edf13..5d76557972 100644 --- a/itest/lnd_sweep_test.go +++ b/itest/lnd_sweep_test.go @@ -113,6 +113,14 @@ func testSweepCPFPAnchorOutgoingTimeout(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) + // Subscribe the invoice. streamCarol := carol.RPC.SubscribeSingleInvoice(payHash[:]) @@ -424,6 +432,14 @@ func testSweepCPFPAnchorIncomingTimeout(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) + // Subscribe the invoice. streamCarol := carol.RPC.SubscribeSingleInvoice(payHash[:]) @@ -758,6 +774,14 @@ func testSweepHTLCs(ht *lntest.HarnessTest) { ht.FundCoins(btcutil.SatoshiPerBitcoin, bob) 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) + // For neutrino backend, we need two more UTXOs for Bob to create his // sweeping txns. if ht.IsNeutrinoBackend() { From 844b100d41cd3941a4c8f6b73f5d99d2251f28db Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 31 Oct 2024 07:29:28 +0800 Subject: [PATCH 109/153] itest: remove loop in `wsTestCaseBiDirectionalSubscription` So we know which open channel operation failed. --- itest/lnd_rest_api_test.go | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/itest/lnd_rest_api_test.go b/itest/lnd_rest_api_test.go index 7c4a3d988e..64a87778f7 100644 --- a/itest/lnd_rest_api_test.go +++ b/itest/lnd_rest_api_test.go @@ -440,7 +440,6 @@ func wsTestCaseBiDirectionalSubscription(ht *lntest.HarnessTest) { msgChan := make(chan *lnrpc.ChannelAcceptResponse, 1) errChan := make(chan error) done := make(chan struct{}) - timeout := time.After(defaultTimeout) // We want to read messages over and over again. We just accept any // channels that are opened. @@ -506,6 +505,7 @@ func wsTestCaseBiDirectionalSubscription(ht *lntest.HarnessTest) { } return } + ht.Log("Finish writing message") // Also send the message on our message channel to make // sure we count it as successful. @@ -525,23 +525,27 @@ func wsTestCaseBiDirectionalSubscription(ht *lntest.HarnessTest) { bob := ht.NewNodeWithCoins("Bob", nil) ht.EnsureConnected(alice, bob) - // Open 3 channels to make sure multiple requests and responses can be - // sent over the web socket. - const numChannels = 3 - for i := 0; i < numChannels; i++ { - ht.OpenChannel( - bob, alice, lntest.OpenChannelParams{Amt: 500000}, - ) - + assertMsgReceived := func() { select { case <-msgChan: case err := <-errChan: ht.Fatalf("Received error from WS: %v", err) - case <-timeout: + case <-time.After(defaultTimeout): ht.Fatalf("Timeout before message was received") } } + + // Open 3 channels to make sure multiple requests and responses can be + // sent over the web socket. + ht.OpenChannel(bob, alice, lntest.OpenChannelParams{Amt: 500000}) + assertMsgReceived() + + ht.OpenChannel(bob, alice, lntest.OpenChannelParams{Amt: 500000}) + assertMsgReceived() + + ht.OpenChannel(bob, alice, lntest.OpenChannelParams{Amt: 500000}) + assertMsgReceived() } func wsTestPingPongTimeout(ht *lntest.HarnessTest) { From c27040132ae544c40f24806f971347ed87e8dbae Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 2 Nov 2024 12:13:32 +0800 Subject: [PATCH 110/153] itest: fix flake in `testRevokedCloseRetributionZeroValueRemoteOutput` We need to mine an empty block as the tx may already have entered the mempool. This should be fixed once we start using the sweeper to handle the justice tx. --- itest/lnd_revocation_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/itest/lnd_revocation_test.go b/itest/lnd_revocation_test.go index 8e2638fe98..5074a3f02d 100644 --- a/itest/lnd_revocation_test.go +++ b/itest/lnd_revocation_test.go @@ -612,7 +612,7 @@ func revokedCloseRetributionRemoteHodlCase(ht *lntest.HarnessTest, // transactions will be in the mempool at this point, we pass 0 // as the last argument, indicating we don't care what's in the // mempool. - ht.MineBlocks(1) + ht.MineEmptyBlocks(1) err = wait.NoError(func() error { txid, err := findJusticeTx() if err != nil { From c6b7aa7b8331675327701ebb50b50ce70c103e56 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sun, 3 Nov 2024 08:21:24 +0800 Subject: [PATCH 111/153] itest: fix flake in `testSwitchOfflineDelivery` The reconnection will happen automatically when the nodes have a channel, so we just ensure the connection instead of reconnecting directly. --- itest/lnd_switch_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/itest/lnd_switch_test.go b/itest/lnd_switch_test.go index 59e8ce9cec..10be216130 100644 --- a/itest/lnd_switch_test.go +++ b/itest/lnd_switch_test.go @@ -103,7 +103,7 @@ func testSwitchOfflineDelivery(ht *lntest.HarnessTest) { ht.DisconnectNodes(s.dave, s.alice) // Then, reconnect them to ensure Dave doesn't just fail back the htlc. - ht.ConnectNodes(s.dave, s.alice) + ht.EnsureConnected(s.dave, s.alice) // Wait to ensure that the payment remain are not failed back after // reconnecting. All node should report the number payments initiated From dda51c2405909f2c02754da6cc65de1f0dd11749 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sun, 3 Nov 2024 13:19:19 +0800 Subject: [PATCH 112/153] itest+routing: fix flake in `runFeeEstimationTestCase` --- itest/lnd_estimate_route_fee_test.go | 2 +- routing/payment_lifecycle.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/itest/lnd_estimate_route_fee_test.go b/itest/lnd_estimate_route_fee_test.go index d8fbcca734..e82e51df2e 100644 --- a/itest/lnd_estimate_route_fee_test.go +++ b/itest/lnd_estimate_route_fee_test.go @@ -376,7 +376,7 @@ func runFeeEstimationTestCase(ht *lntest.HarnessTest, ) feeReq = &routerrpc.RouteFeeRequest{ PaymentRequest: payReqs[0], - Timeout: 10, + Timeout: 60, } } else { feeReq = &routerrpc.RouteFeeRequest{ diff --git a/routing/payment_lifecycle.go b/routing/payment_lifecycle.go index b94b5c3917..4ae94c5814 100644 --- a/routing/payment_lifecycle.go +++ b/routing/payment_lifecycle.go @@ -340,7 +340,7 @@ func (p *paymentLifecycle) checkContext(ctx context.Context) error { if errors.Is(ctx.Err(), context.DeadlineExceeded) { reason = channeldb.FailureReasonTimeout log.Warnf("Payment attempt not completed before "+ - "timeout, id=%s", p.identifier.String()) + "context timeout, id=%s", p.identifier.String()) } else { reason = channeldb.FailureReasonCanceled log.Warnf("Payment attempt context canceled, id=%s", From d388c8821e734bfe0b90587c593ea959653d1be7 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sun, 3 Nov 2024 13:34:16 +0800 Subject: [PATCH 113/153] itest: use `ht.CreateSimpleNetwork` whenever applicable So we won't forget to assert the topology after opening a chain of channels. --- itest/lnd_forward_interceptor_test.go | 157 +++++++++----------------- itest/lnd_payment_test.go | 32 ++---- 2 files changed, 67 insertions(+), 122 deletions(-) diff --git a/itest/lnd_forward_interceptor_test.go b/itest/lnd_forward_interceptor_test.go index b024df1020..8362647e8f 100644 --- a/itest/lnd_forward_interceptor_test.go +++ b/itest/lnd_forward_interceptor_test.go @@ -42,23 +42,24 @@ type interceptorTestCase struct { // testForwardInterceptorDedupHtlc tests that upon reconnection, duplicate // HTLCs aren't re-notified using the HTLC interceptor API. func testForwardInterceptorDedupHtlc(ht *lntest.HarnessTest) { - // Initialize the test context with 3 connected nodes. - ts := newInterceptorTestScenario(ht) + const chanAmt = btcutil.Amount(300000) + p := lntest.OpenChannelParams{Amt: chanAmt} - alice, bob, carol := ts.alice, ts.bob, ts.carol + // Initialize the test context with 3 connected nodes. + cfgs := [][]string{nil, nil, nil} // Open and wait for channels. - const chanAmt = btcutil.Amount(300000) - p := lntest.OpenChannelParams{Amt: chanAmt} - reqs := []*lntest.OpenChannelRequest{ - {Local: alice, Remote: bob, Param: p}, - {Local: bob, Remote: carol, Param: p}, - } - resp := ht.OpenMultiChannelsAsync(reqs) - cpAB, cpBC := resp[0], resp[1] + chanPoints, nodes := ht.CreateSimpleNetwork(cfgs, p) + alice, bob, carol := nodes[0], nodes[1], nodes[2] + cpAB := chanPoints[0] - // Make sure Alice is aware of channel Bob=>Carol. - ht.AssertChannelInGraph(alice, cpBC) + // Init the scenario. + ts := &interceptorTestScenario{ + ht: ht, + alice: alice, + bob: bob, + carol: carol, + } // Connect the interceptor. interceptor, cancelInterceptor := bob.RPC.HtlcInterceptor() @@ -190,22 +191,24 @@ func testForwardInterceptorDedupHtlc(ht *lntest.HarnessTest) { // 4. When Interceptor disconnects it resumes all held htlcs, which result in // valid payment (invoice is settled). func testForwardInterceptorBasic(ht *lntest.HarnessTest) { - ts := newInterceptorTestScenario(ht) + const chanAmt = btcutil.Amount(300000) + p := lntest.OpenChannelParams{Amt: chanAmt} - alice, bob, carol := ts.alice, ts.bob, ts.carol + // Initialize the test context with 3 connected nodes. + cfgs := [][]string{nil, nil, nil} // Open and wait for channels. - const chanAmt = btcutil.Amount(300000) - p := lntest.OpenChannelParams{Amt: chanAmt} - reqs := []*lntest.OpenChannelRequest{ - {Local: alice, Remote: bob, Param: p}, - {Local: bob, Remote: carol, Param: p}, - } - resp := ht.OpenMultiChannelsAsync(reqs) - cpAB, cpBC := resp[0], resp[1] + chanPoints, nodes := ht.CreateSimpleNetwork(cfgs, p) + alice, bob, carol := nodes[0], nodes[1], nodes[2] + cpAB := chanPoints[0] - // Make sure Alice is aware of channel Bob=>Carol. - ht.AssertChannelInGraph(alice, cpBC) + // Init the scenario. + ts := &interceptorTestScenario{ + ht: ht, + alice: alice, + bob: bob, + carol: carol, + } // Connect the interceptor. interceptor, cancelInterceptor := bob.RPC.HtlcInterceptor() @@ -346,23 +349,23 @@ func testForwardInterceptorBasic(ht *lntest.HarnessTest) { // testForwardInterceptorModifiedHtlc tests that the interceptor can modify the // amount and custom records of an intercepted HTLC and resume it. func testForwardInterceptorModifiedHtlc(ht *lntest.HarnessTest) { - // Initialize the test context with 3 connected nodes. - ts := newInterceptorTestScenario(ht) + const chanAmt = btcutil.Amount(300000) + p := lntest.OpenChannelParams{Amt: chanAmt} - alice, bob, carol := ts.alice, ts.bob, ts.carol + // Initialize the test context with 3 connected nodes. + cfgs := [][]string{nil, nil, nil} // Open and wait for channels. - const chanAmt = btcutil.Amount(300000) - p := lntest.OpenChannelParams{Amt: chanAmt} - reqs := []*lntest.OpenChannelRequest{ - {Local: alice, Remote: bob, Param: p}, - {Local: bob, Remote: carol, Param: p}, - } - resp := ht.OpenMultiChannelsAsync(reqs) - cpBC := resp[1] + _, nodes := ht.CreateSimpleNetwork(cfgs, p) + alice, bob, carol := nodes[0], nodes[1], nodes[2] - // Make sure Alice is aware of channel Bob=>Carol. - ht.AssertChannelInGraph(alice, cpBC) + // Init the scenario. + ts := &interceptorTestScenario{ + ht: ht, + alice: alice, + bob: bob, + carol: carol, + } // Connect an interceptor to Bob's node. bobInterceptor, cancelBobInterceptor := bob.RPC.HtlcInterceptor() @@ -449,24 +452,15 @@ func testForwardInterceptorModifiedHtlc(ht *lntest.HarnessTest) { // wire custom records provided by the sender of a payment as part of the // update_add_htlc message. func testForwardInterceptorWireRecords(ht *lntest.HarnessTest) { - // Initialize the test context with 3 connected nodes. - ts := newInterceptorTestScenario(ht) - - alice, bob, carol, dave := ts.alice, ts.bob, ts.carol, ts.dave - - // Open and wait for channels. const chanAmt = btcutil.Amount(300000) p := lntest.OpenChannelParams{Amt: chanAmt} - reqs := []*lntest.OpenChannelRequest{ - {Local: alice, Remote: bob, Param: p}, - {Local: bob, Remote: carol, Param: p}, - {Local: carol, Remote: dave, Param: p}, - } - resp := ht.OpenMultiChannelsAsync(reqs) - cpBC := resp[1] - // Make sure Alice is aware of channel Bob=>Carol. - ht.AssertChannelInGraph(alice, cpBC) + // Initialize the test context with 4 connected nodes. + cfgs := [][]string{nil, nil, nil, nil} + + // Open and wait for channels. + _, nodes := ht.CreateSimpleNetwork(cfgs, p) + alice, bob, carol, dave := nodes[0], nodes[1], nodes[2], nodes[3] // Connect an interceptor to Bob's node. bobInterceptor, cancelBobInterceptor := bob.RPC.HtlcInterceptor() @@ -574,25 +568,15 @@ func testForwardInterceptorWireRecords(ht *lntest.HarnessTest) { // update_add_htlc message and that those records are persisted correctly and // re-sent on node restart. func testForwardInterceptorRestart(ht *lntest.HarnessTest) { - // Initialize the test context with 3 connected nodes. - ts := newInterceptorTestScenario(ht) - - alice, bob, carol, dave := ts.alice, ts.bob, ts.carol, ts.dave - - // Open and wait for channels. const chanAmt = btcutil.Amount(300000) p := lntest.OpenChannelParams{Amt: chanAmt} - reqs := []*lntest.OpenChannelRequest{ - {Local: alice, Remote: bob, Param: p}, - {Local: bob, Remote: carol, Param: p}, - {Local: carol, Remote: dave, Param: p}, - } - resp := ht.OpenMultiChannelsAsync(reqs) - cpBC, cpCD := resp[1], resp[2] - // Make sure Alice is aware of channels Bob=>Carol and Carol=>Dave. - ht.AssertChannelInGraph(alice, cpBC) - ht.AssertChannelInGraph(alice, cpCD) + // Initialize the test context with 4 connected nodes. + cfgs := [][]string{nil, nil, nil, nil} + + // Open and wait for channels. + _, nodes := ht.CreateSimpleNetwork(cfgs, p) + alice, bob, carol, dave := nodes[0], nodes[1], nodes[2], nodes[3] // Connect an interceptor to Bob's node. bobInterceptor, cancelBobInterceptor := bob.RPC.HtlcInterceptor() @@ -727,39 +711,8 @@ func testForwardInterceptorRestart(ht *lntest.HarnessTest) { // interceptorTestScenario is a helper struct to hold the test context and // provide the needed functionality. type interceptorTestScenario struct { - ht *lntest.HarnessTest - alice, bob, carol, dave *node.HarnessNode -} - -// newInterceptorTestScenario initializes a new test scenario with three nodes -// and connects them to have the following topology, -// -// Alice --> Bob --> Carol --> Dave -// -// Among them, Alice and Bob are standby nodes and Carol is a new node. -func newInterceptorTestScenario( - ht *lntest.HarnessTest) *interceptorTestScenario { - - alice := ht.NewNodeWithCoins("Alice", nil) - bob := ht.NewNodeWithCoins("bob", nil) - - carol := ht.NewNode("carol", nil) - dave := ht.NewNode("dave", nil) - - ht.EnsureConnected(alice, bob) - ht.EnsureConnected(bob, carol) - ht.EnsureConnected(carol, dave) - - // So that carol can open channels. - ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) - - return &interceptorTestScenario{ - ht: ht, - alice: alice, - bob: bob, - carol: carol, - dave: dave, - } + ht *lntest.HarnessTest + alice, bob, carol *node.HarnessNode } // prepareTestCases prepares 4 tests: diff --git a/itest/lnd_payment_test.go b/itest/lnd_payment_test.go index 74adc4ffad..8f9b2bc748 100644 --- a/itest/lnd_payment_test.go +++ b/itest/lnd_payment_test.go @@ -1070,23 +1070,16 @@ func assertChannelState(ht *lntest.HarnessTest, hn *node.HarnessNode, // 5.) Alice observes a failed OR succeeded payment with failure reason // FAILURE_REASON_CANCELED which suppresses further payment attempts. func testPaymentFailureReasonCanceled(ht *lntest.HarnessTest) { - // Initialize the test context with 3 connected nodes. - ts := newInterceptorTestScenario(ht) - - alice, bob, carol := ts.alice, ts.bob, ts.carol - - // Open and wait for channels. const chanAmt = btcutil.Amount(300000) p := lntest.OpenChannelParams{Amt: chanAmt} - reqs := []*lntest.OpenChannelRequest{ - {Local: alice, Remote: bob, Param: p}, - {Local: bob, Remote: carol, Param: p}, - } - resp := ht.OpenMultiChannelsAsync(reqs) - cpAB, cpBC := resp[0], resp[1] - // Make sure Alice is aware of channel Bob=>Carol. - ht.AssertChannelInGraph(alice, cpBC) + // Initialize the test context with 3 connected nodes. + cfgs := [][]string{nil, nil, nil} + + // Open and wait for channels. + chanPoints, nodes := ht.CreateSimpleNetwork(cfgs, p) + alice, bob, carol := nodes[0], nodes[1], nodes[2] + cpAB := chanPoints[0] // Connect the interceptor. interceptor, cancelInterceptor := bob.RPC.HtlcInterceptor() @@ -1096,7 +1089,8 @@ func testPaymentFailureReasonCanceled(ht *lntest.HarnessTest) { // htlc even though the payment context was canceled before invoice // settlement. sendPaymentInterceptAndCancel( - ht, ts, cpAB, routerrpc.ResolveHoldForwardAction_RESUME, + ht, alice, bob, carol, cpAB, + routerrpc.ResolveHoldForwardAction_RESUME, lnrpc.Payment_SUCCEEDED, interceptor, ) @@ -1106,20 +1100,18 @@ func testPaymentFailureReasonCanceled(ht *lntest.HarnessTest) { // Note that we'd have to reset Alice's mission control if we tested the // htlc fail case before the htlc resume case. sendPaymentInterceptAndCancel( - ht, ts, cpAB, routerrpc.ResolveHoldForwardAction_FAIL, + ht, alice, bob, carol, cpAB, + routerrpc.ResolveHoldForwardAction_FAIL, lnrpc.Payment_FAILED, interceptor, ) } func sendPaymentInterceptAndCancel(ht *lntest.HarnessTest, - ts *interceptorTestScenario, cpAB *lnrpc.ChannelPoint, + alice, bob, carol *node.HarnessNode, cpAB *lnrpc.ChannelPoint, interceptorAction routerrpc.ResolveHoldForwardAction, expectedPaymentStatus lnrpc.Payment_PaymentStatus, interceptor rpc.InterceptorClient) { - // Prepare the test cases. - alice, bob, carol := ts.alice, ts.bob, ts.carol - // Prepare the test cases. addResponse := carol.RPC.AddInvoice(&lnrpc.Invoice{ ValueMsat: 1000, From 45ee9e5667a7cc4ee8d201fd4b973961a21a77d5 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Mon, 4 Nov 2024 11:14:47 +0800 Subject: [PATCH 114/153] itest: put mpp tests in one file --- itest/lnd_mpp_test.go | 105 ++++++++++++++++++++ itest/lnd_send_multi_path_payment_test.go | 115 ---------------------- 2 files changed, 105 insertions(+), 115 deletions(-) delete mode 100644 itest/lnd_send_multi_path_payment_test.go diff --git a/itest/lnd_mpp_test.go b/itest/lnd_mpp_test.go index 99eb1eb599..0316e253ae 100644 --- a/itest/lnd_mpp_test.go +++ b/itest/lnd_mpp_test.go @@ -1,6 +1,7 @@ package itest import ( + "encoding/hex" "time" "github.com/btcsuite/btcd/btcutil" @@ -14,6 +15,110 @@ import ( "github.com/stretchr/testify/require" ) +// testSendMultiPathPayment tests that we are able to successfully route a +// payment using multiple shards across different paths. +func testSendMultiPathPayment(ht *lntest.HarnessTest) { + mts := newMppTestScenario(ht) + + const paymentAmt = btcutil.Amount(300000) + + // Set up a network with three different paths Alice <-> Bob. Channel + // capacities are set such that the payment can only succeed if (at + // least) three paths are used. + // + // _ Eve _ + // / \ + // Alice -- Carol ---- Bob + // \ / + // \__ Dave ____/ + // + req := &mppOpenChannelRequest{ + amtAliceCarol: 285000, + amtAliceDave: 155000, + amtCarolBob: 200000, + amtCarolEve: 155000, + amtDaveBob: 155000, + amtEveBob: 155000, + } + mts.openChannels(req) + chanPointAliceDave := mts.channelPoints[1] + + // Increase Dave's fee to make the test deterministic. Otherwise, it + // would be unpredictable whether pathfinding would go through Charlie + // or Dave for the first shard. + expectedPolicy := &lnrpc.RoutingPolicy{ + FeeBaseMsat: 500_000, + FeeRateMilliMsat: int64(0.001 * 1_000_000), + TimeLockDelta: 40, + MinHtlc: 1000, // default value + MaxHtlcMsat: 133_650_000, + } + mts.dave.UpdateGlobalPolicy(expectedPolicy) + + // Make sure Alice has heard it. + ht.AssertChannelPolicyUpdate( + mts.alice, mts.dave, expectedPolicy, chanPointAliceDave, false, + ) + + // Our first test will be Alice paying Bob using a SendPayment call. + // Let Bob create an invoice for Alice to pay. + payReqs, rHashes, invoices := ht.CreatePayReqs(mts.bob, paymentAmt, 1) + + rHash := rHashes[0] + payReq := payReqs[0] + + sendReq := &routerrpc.SendPaymentRequest{ + PaymentRequest: payReq, + MaxParts: 10, + TimeoutSeconds: 60, + FeeLimitMsat: noFeeLimitMsat, + } + payment := ht.SendPaymentAssertSettled(mts.alice, sendReq) + + // Make sure we got the preimage. + require.Equal(ht, hex.EncodeToString(invoices[0].RPreimage), + payment.PaymentPreimage, "preimage doesn't match") + + // Check that Alice split the payment in at least three shards. Because + // the hand-off of the htlc to the link is asynchronous (via a mailbox), + // there is some non-determinism in the process. Depending on whether + // the new pathfinding round is started before or after the htlc is + // locked into the channel, different sharding may occur. Therefore we + // can only check if the number of shards isn't below the theoretical + // minimum. + succeeded := 0 + for _, htlc := range payment.Htlcs { + if htlc.Status == lnrpc.HTLCAttempt_SUCCEEDED { + succeeded++ + } + } + + const minExpectedShards = 3 + require.GreaterOrEqual(ht, succeeded, minExpectedShards, + "expected shards not reached") + + // Make sure Bob show the invoice as settled for the full amount. + inv := mts.bob.RPC.LookupInvoice(rHash) + + require.EqualValues(ht, paymentAmt, inv.AmtPaidSat, + "incorrect payment amt") + + require.Equal(ht, lnrpc.Invoice_SETTLED, inv.State, + "Invoice not settled") + + settled := 0 + for _, htlc := range inv.Htlcs { + if htlc.State == lnrpc.InvoiceHTLCState_SETTLED { + settled++ + } + } + require.Equal(ht, succeeded, settled, + "num of HTLCs wrong") + + // Finally, close all channels. + mts.closeChannels() +} + // testSendToRouteMultiPath tests that we are able to successfully route a // payment using multiple shards across different paths, by using SendToRoute. func testSendToRouteMultiPath(ht *lntest.HarnessTest) { diff --git a/itest/lnd_send_multi_path_payment_test.go b/itest/lnd_send_multi_path_payment_test.go deleted file mode 100644 index 935997c8d2..0000000000 --- a/itest/lnd_send_multi_path_payment_test.go +++ /dev/null @@ -1,115 +0,0 @@ -package itest - -import ( - "encoding/hex" - - "github.com/btcsuite/btcd/btcutil" - "github.com/lightningnetwork/lnd/lnrpc" - "github.com/lightningnetwork/lnd/lnrpc/routerrpc" - "github.com/lightningnetwork/lnd/lntest" - "github.com/stretchr/testify/require" -) - -// testSendMultiPathPayment tests that we are able to successfully route a -// payment using multiple shards across different paths. -func testSendMultiPathPayment(ht *lntest.HarnessTest) { - mts := newMppTestScenario(ht) - - const paymentAmt = btcutil.Amount(300000) - - // Set up a network with three different paths Alice <-> Bob. Channel - // capacities are set such that the payment can only succeed if (at - // least) three paths are used. - // - // _ Eve _ - // / \ - // Alice -- Carol ---- Bob - // \ / - // \__ Dave ____/ - // - req := &mppOpenChannelRequest{ - amtAliceCarol: 285000, - amtAliceDave: 155000, - amtCarolBob: 200000, - amtCarolEve: 155000, - amtDaveBob: 155000, - amtEveBob: 155000, - } - mts.openChannels(req) - chanPointAliceDave := mts.channelPoints[1] - - // Increase Dave's fee to make the test deterministic. Otherwise, it - // would be unpredictable whether pathfinding would go through Charlie - // or Dave for the first shard. - expectedPolicy := &lnrpc.RoutingPolicy{ - FeeBaseMsat: 500_000, - FeeRateMilliMsat: int64(0.001 * 1_000_000), - TimeLockDelta: 40, - MinHtlc: 1000, // default value - MaxHtlcMsat: 133_650_000, - } - mts.dave.UpdateGlobalPolicy(expectedPolicy) - - // Make sure Alice has heard it. - ht.AssertChannelPolicyUpdate( - mts.alice, mts.dave, expectedPolicy, chanPointAliceDave, false, - ) - - // Our first test will be Alice paying Bob using a SendPayment call. - // Let Bob create an invoice for Alice to pay. - payReqs, rHashes, invoices := ht.CreatePayReqs(mts.bob, paymentAmt, 1) - - rHash := rHashes[0] - payReq := payReqs[0] - - sendReq := &routerrpc.SendPaymentRequest{ - PaymentRequest: payReq, - MaxParts: 10, - TimeoutSeconds: 60, - FeeLimitMsat: noFeeLimitMsat, - } - payment := ht.SendPaymentAssertSettled(mts.alice, sendReq) - - // Make sure we got the preimage. - require.Equal(ht, hex.EncodeToString(invoices[0].RPreimage), - payment.PaymentPreimage, "preimage doesn't match") - - // Check that Alice split the payment in at least three shards. Because - // the hand-off of the htlc to the link is asynchronous (via a mailbox), - // there is some non-determinism in the process. Depending on whether - // the new pathfinding round is started before or after the htlc is - // locked into the channel, different sharding may occur. Therefore we - // can only check if the number of shards isn't below the theoretical - // minimum. - succeeded := 0 - for _, htlc := range payment.Htlcs { - if htlc.Status == lnrpc.HTLCAttempt_SUCCEEDED { - succeeded++ - } - } - - const minExpectedShards = 3 - require.GreaterOrEqual(ht, succeeded, minExpectedShards, - "expected shards not reached") - - // Make sure Bob show the invoice as settled for the full amount. - inv := mts.bob.RPC.LookupInvoice(rHash) - - require.EqualValues(ht, paymentAmt, inv.AmtPaidSat, - "incorrect payment amt") - - require.Equal(ht, lnrpc.Invoice_SETTLED, inv.State, - "Invoice not settled") - - settled := 0 - for _, htlc := range inv.Htlcs { - if htlc.State == lnrpc.InvoiceHTLCState_SETTLED { - settled++ - } - } - require.Equal(ht, succeeded, settled, - "num of HTLCs wrong") - - // Finally, close all channels. - mts.closeChannels() -} From 3c4b63a439adaafbf2e3cd72b0e9435471b35842 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 8 Nov 2024 16:52:59 +0800 Subject: [PATCH 115/153] lntest+itest: remove `AssertNumActiveEdges` This is no longer needed since we don't have standby nodes, plus it's causing panic in windows build due to `edge.Policy` being nil. --- itest/lnd_channel_graph_test.go | 6 ++-- itest/lnd_mpp_test.go | 2 +- itest/lnd_open_channel_test.go | 4 +-- itest/lnd_route_blinding_test.go | 6 ++-- itest/lnd_routing_test.go | 12 +++---- lntest/harness_assertion.go | 54 -------------------------------- 6 files changed, 15 insertions(+), 69 deletions(-) diff --git a/itest/lnd_channel_graph_test.go b/itest/lnd_channel_graph_test.go index a7ad37aca7..b21da219ab 100644 --- a/itest/lnd_channel_graph_test.go +++ b/itest/lnd_channel_graph_test.go @@ -237,17 +237,17 @@ func testUnannouncedChannels(ht *lntest.HarnessTest) { ht.WaitForChannelOpenEvent(chanOpenUpdate) // Alice should have 1 edge in her graph. - ht.AssertNumActiveEdges(alice, 1, true) + ht.AssertNumEdges(alice, 1, true) // Channels should not be announced yet, hence Alice should have no // announced edges in her graph. - ht.AssertNumActiveEdges(alice, 0, false) + ht.AssertNumEdges(alice, 0, false) // Mine 4 more blocks, and check that the channel is now announced. ht.MineBlocks(4) // Give the network a chance to learn that auth proof is confirmed. - ht.AssertNumActiveEdges(alice, 1, false) + ht.AssertNumEdges(alice, 1, false) } func testGraphTopologyNotifications(ht *lntest.HarnessTest) { diff --git a/itest/lnd_mpp_test.go b/itest/lnd_mpp_test.go index 0316e253ae..7bc23da2a8 100644 --- a/itest/lnd_mpp_test.go +++ b/itest/lnd_mpp_test.go @@ -404,7 +404,7 @@ func (m *mppTestScenario) openChannels(r *mppOpenChannelRequest) { } // Each node should have exactly 6 edges. - m.ht.AssertNumActiveEdges(hn, len(m.channelPoints), false) + m.ht.AssertNumEdges(hn, len(m.channelPoints), false) } } diff --git a/itest/lnd_open_channel_test.go b/itest/lnd_open_channel_test.go index 66930a6c24..47311b25ea 100644 --- a/itest/lnd_open_channel_test.go +++ b/itest/lnd_open_channel_test.go @@ -88,7 +88,7 @@ func testOpenChannelAfterReorg(ht *lntest.HarnessTest) { ht.AssertChannelInGraph(bob, chanPoint) // Alice should now have 1 edge in her graph. - ht.AssertNumActiveEdges(alice, 1, true) + ht.AssertNumEdges(alice, 1, true) // Now we disconnect Alice's chain backend from the original miner, and // connect the two miners together. Since the temporary miner knows @@ -116,7 +116,7 @@ func testOpenChannelAfterReorg(ht *lntest.HarnessTest) { // Since the fundingtx was reorged out, Alice should now have no edges // in her graph. - ht.AssertNumActiveEdges(alice, 0, true) + ht.AssertNumEdges(alice, 0, true) // Cleanup by mining the funding tx again, then closing the channel. block = ht.MineBlocksAndAssertNumTxes(1, 1)[0] diff --git a/itest/lnd_route_blinding_test.go b/itest/lnd_route_blinding_test.go index 189130a89b..1948271814 100644 --- a/itest/lnd_route_blinding_test.go +++ b/itest/lnd_route_blinding_test.go @@ -938,7 +938,7 @@ func testMPPToSingleBlindedPath(ht *lntest.HarnessTest) { } // Each node should have exactly numPublic edges. - ht.AssertNumActiveEdges(hn, numPublic, false) + ht.AssertNumEdges(hn, numPublic, false) } // Make Dave create an invoice with a blinded path for Alice to pay. @@ -1109,7 +1109,7 @@ func testBlindedRouteDummyHops(ht *lntest.HarnessTest) { } // Each node should have exactly 5 edges. - ht.AssertNumActiveEdges(hn, len(channelPoints), false) + ht.AssertNumEdges(hn, len(channelPoints), false) } // Make Dave create an invoice with a blinded path for Alice to pay. @@ -1279,7 +1279,7 @@ func testMPPToMultipleBlindedPaths(ht *lntest.HarnessTest) { } // Each node should have exactly 5 edges. - ht.AssertNumActiveEdges(hn, len(channelPoints), false) + ht.AssertNumEdges(hn, len(channelPoints), false) } // Ok now make a payment that must be split to succeed. diff --git a/itest/lnd_routing_test.go b/itest/lnd_routing_test.go index 63c5d54189..950b24d24b 100644 --- a/itest/lnd_routing_test.go +++ b/itest/lnd_routing_test.go @@ -589,12 +589,12 @@ func testPrivateChannels(ht *lntest.HarnessTest) { // Carol and Alice should know about 4, while Bob and Dave should only // know about 3, since one channel is private. - ht.AssertNumActiveEdges(alice, 4, true) - ht.AssertNumActiveEdges(alice, 3, false) - ht.AssertNumActiveEdges(bob, 3, true) - ht.AssertNumActiveEdges(carol, 4, true) - ht.AssertNumActiveEdges(carol, 3, false) - ht.AssertNumActiveEdges(dave, 3, true) + ht.AssertNumEdges(alice, 4, true) + ht.AssertNumEdges(alice, 3, false) + ht.AssertNumEdges(bob, 3, true) + ht.AssertNumEdges(carol, 4, true) + ht.AssertNumEdges(carol, 3, false) + ht.AssertNumEdges(dave, 3, true) } // testInvoiceRoutingHints tests that the routing hints for an invoice are diff --git a/lntest/harness_assertion.go b/lntest/harness_assertion.go index 5d502b5be4..c684941f73 100644 --- a/lntest/harness_assertion.go +++ b/lntest/harness_assertion.go @@ -19,7 +19,6 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/channeldb" - "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" "github.com/lightningnetwork/lnd/lnrpc/routerrpc" @@ -241,59 +240,6 @@ func (h *HarnessTest) EnsureConnected(a, b *node.HarnessNode) { h.AssertPeerConnected(b, a) } -// AssertNumActiveEdges checks that an expected number of active edges can be -// found in the node specified. -func (h *HarnessTest) AssertNumActiveEdges(hn *node.HarnessNode, - expected int, includeUnannounced bool) []*lnrpc.ChannelEdge { - - var edges []*lnrpc.ChannelEdge - - old := hn.State.Edge.Public - if includeUnannounced { - old = hn.State.Edge.Total - } - - // filterDisabled is a helper closure that filters out disabled - // channels. - filterDisabled := func(edge *lnrpc.ChannelEdge) bool { - if edge.Node1Policy != nil && edge.Node1Policy.Disabled { - return false - } - if edge.Node2Policy != nil && edge.Node2Policy.Disabled { - return false - } - - return true - } - - err := wait.NoError(func() error { - req := &lnrpc.ChannelGraphRequest{ - IncludeUnannounced: includeUnannounced, - } - resp := hn.RPC.DescribeGraph(req) - activeEdges := fn.Filter(filterDisabled, resp.Edges) - total := len(activeEdges) - - if total-old == expected { - if expected != 0 { - // NOTE: assume edges come in ascending order - // that the old edges are at the front of the - // slice. - edges = activeEdges[old:] - } - - return nil - } - - return errNumNotMatched(hn.Name(), "num of channel edges", - expected, total-old, total, old) - }, DefaultTimeout) - - require.NoError(h, err, "timeout while checking for edges") - - return edges -} - // AssertNumEdges checks that an expected number of edges can be found in the // node specified. func (h *HarnessTest) AssertNumEdges(hn *node.HarnessNode, From e005582960e3a33d999e432e2c9e1cbf4a502999 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sun, 10 Nov 2024 23:27:25 +0800 Subject: [PATCH 116/153] itest: fix flake in runPsbtChanFundingWithNodes --- itest/lnd_psbt_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/itest/lnd_psbt_test.go b/itest/lnd_psbt_test.go index a91ddef7fb..f8ddc26839 100644 --- a/itest/lnd_psbt_test.go +++ b/itest/lnd_psbt_test.go @@ -269,6 +269,9 @@ func runPsbtChanFundingWithNodes(ht *lntest.HarnessTest, carol, txHash := finalTx.TxHash() block := ht.MineBlocksAndAssertNumTxes(6, 1)[0] ht.AssertTxInBlock(block, txHash) + + ht.AssertChannelActive(carol, chanPoint) + ht.AssertChannelActive(carol, chanPoint2) ht.AssertChannelInGraph(carol, chanPoint) ht.AssertChannelInGraph(carol, chanPoint2) From 4f7d952980b35f6fb2c4b518e6881af4341dbde3 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 16 Nov 2024 12:23:21 +0800 Subject: [PATCH 117/153] itest: fix flake in `testPrivateUpdateAlias` --- itest/lnd_zero_conf_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/itest/lnd_zero_conf_test.go b/itest/lnd_zero_conf_test.go index 8ec19945ca..c7f4c59a78 100644 --- a/itest/lnd_zero_conf_test.go +++ b/itest/lnd_zero_conf_test.go @@ -621,6 +621,9 @@ func testPrivateUpdateAlias(ht *lntest.HarnessTest, // // TODO(yy): further investigate this sleep. time.Sleep(time.Second * 5) + + // Make sure Eve has heard about this public channel. + ht.AssertChannelInGraph(eve, fundingPoint2) } // Dave creates an invoice that Eve will pay. From 076885baa4861573958378e77216c96c97d74c68 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 16 Nov 2024 13:36:59 +0800 Subject: [PATCH 118/153] itest: fix flake in `update_pending_open_channels` --- itest/list_on_test.go | 8 ++++++-- itest/lnd_open_channel_test.go | 29 ++++++----------------------- 2 files changed, 12 insertions(+), 25 deletions(-) diff --git a/itest/list_on_test.go b/itest/list_on_test.go index ac1653bfc8..b3952cad9e 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -569,8 +569,12 @@ var allTestCases = []*lntest.TestCase{ TestFunc: testChannelUtxoSelection, }, { - Name: "update pending open channels", - TestFunc: testUpdateOnPendingOpenChannels, + Name: "update pending open channels on funder side", + TestFunc: testUpdateOnFunderPendingOpenChannels, + }, + { + Name: "update pending open channels on fundee side", + TestFunc: testUpdateOnFundeePendingOpenChannels, }, { Name: "blinded payment htlc re-forward", diff --git a/itest/lnd_open_channel_test.go b/itest/lnd_open_channel_test.go index 47311b25ea..3142e09eb2 100644 --- a/itest/lnd_open_channel_test.go +++ b/itest/lnd_open_channel_test.go @@ -486,27 +486,6 @@ func runBasicChannelCreationAndUpdates(ht *lntest.HarnessTest, ) } -// testUpdateOnPendingOpenChannels checks that `update_add_htlc` followed by -// `channel_ready` is properly handled. In specific, when a node is in a state -// that it's still processing a remote `channel_ready` message, meanwhile an -// `update_add_htlc` is received, this HTLC message is cached and settled once -// processing `channel_ready` is complete. -func testUpdateOnPendingOpenChannels(ht *lntest.HarnessTest) { - // Test funder's behavior. Funder sees the channel pending, but fundee - // sees it active and sends an HTLC. - ht.Run("pending on funder side", func(t *testing.T) { - st := ht.Subtest(t) - testUpdateOnFunderPendingOpenChannels(st) - }) - - // Test fundee's behavior. Fundee sees the channel pending, but funder - // sees it active and sends an HTLC. - ht.Run("pending on fundee side", func(t *testing.T) { - st := ht.Subtest(t) - testUpdateOnFundeePendingOpenChannels(st) - }) -} - // testUpdateOnFunderPendingOpenChannels checks that when the fundee sends an // `update_add_htlc` followed by `channel_ready` while the funder is still // processing the fundee's `channel_ready`, the HTLC will be cached and @@ -530,7 +509,8 @@ func testUpdateOnFunderPendingOpenChannels(ht *lntest.HarnessTest) { Amt: funding.MaxBtcFundingAmount, PushAmt: funding.MaxBtcFundingAmount / 2, } - ht.OpenChannelAssertPending(alice, bob, params) + pending := ht.OpenChannelAssertPending(alice, bob, params) + chanPoint := lntest.ChanPointFromPendingUpdate(pending) // Alice and Bob should both consider the channel pending open. ht.AssertNumPendingOpenChannels(alice, 1) @@ -548,6 +528,7 @@ func testUpdateOnFunderPendingOpenChannels(ht *lntest.HarnessTest) { // Bob will consider the channel open as there's no wait time to send // and receive Alice's channel_ready message. ht.AssertNumPendingOpenChannels(bob, 0) + ht.AssertChannelInGraph(bob, chanPoint) // Alice and Bob now have different view of the channel. For Bob, // since the channel_ready messages are processed, he will have a @@ -604,7 +585,8 @@ func testUpdateOnFundeePendingOpenChannels(ht *lntest.HarnessTest) { params := lntest.OpenChannelParams{ Amt: funding.MaxBtcFundingAmount, } - ht.OpenChannelAssertPending(alice, bob, params) + pending := ht.OpenChannelAssertPending(alice, bob, params) + chanPoint := lntest.ChanPointFromPendingUpdate(pending) // Alice and Bob should both consider the channel pending open. ht.AssertNumPendingOpenChannels(alice, 1) @@ -616,6 +598,7 @@ func testUpdateOnFundeePendingOpenChannels(ht *lntest.HarnessTest) { // Alice will consider the channel open as there's no wait time to send // and receive Bob's channel_ready message. ht.AssertNumPendingOpenChannels(alice, 0) + ht.AssertChannelInGraph(alice, chanPoint) // TODO(yy): we've prematurely marked the channel as open before // processing channel ready messages. We need to mark it as open after From cb3b28552223dd904fd0f3eef9287a74520eab6f Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Wed, 20 Nov 2024 21:11:06 +0800 Subject: [PATCH 119/153] lntest: increase `rpcmaxwebsockets` for `btcd` This has been seen in the itest which can lead to the node startup failure, ``` 2024-11-20 18:55:15.727 [INF] RPCS: Max websocket clients exceeded [25] - disconnecting client 127.0.0.1:57224 ``` --- lntest/btcd.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lntest/btcd.go b/lntest/btcd.go index 6b607978eb..91b25411c9 100644 --- a/lntest/btcd.go +++ b/lntest/btcd.go @@ -94,6 +94,14 @@ func NewBackend(miner string, netParams *chaincfg.Params) ( "--nobanning", // Don't disconnect if a reply takes too long. "--nostalldetect", + + // The default max num of websockets is 25, but the closed + // connections are not cleaned up immediately so we double the + // size. + // + // TODO(yy): fix this in `btcd` to clean up the stale + // connections. + "--rpcmaxwebsockets=50", } chainBackend, err := rpctest.New( netParams, nil, args, node.GetBtcdBinary(), From a6bbb3f4b011c1477f79409b162bb9bdc4154f5e Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 21 Nov 2024 13:45:34 +0800 Subject: [PATCH 120/153] itest: document details about MPP-related tests This is needed so we can have one place to fix the flakes found in the MPP-related tests, which is fixed in the following commit. --- itest/lnd_amp_test.go | 47 +++------------ itest/lnd_mpp_test.go | 136 +++++++++++++++++++++++++++++++++--------- 2 files changed, 116 insertions(+), 67 deletions(-) diff --git a/itest/lnd_amp_test.go b/itest/lnd_amp_test.go index 4b4cfb5a29..cc54d1c9d7 100644 --- a/itest/lnd_amp_test.go +++ b/itest/lnd_amp_test.go @@ -47,8 +47,6 @@ func testSendPaymentAMPInvoiceCase(ht *lntest.HarnessTest, req := &lnrpc.InvoiceSubscription{} bobInvoiceSubscription := mts.bob.RPC.SubscribeInvoices(req) - const paymentAmt = btcutil.Amount(300000) - // Set up a network with three different paths Alice <-> Bob. Channel // capacities are set such that the payment can only succeed if (at // least) three paths are used. @@ -59,15 +57,8 @@ func testSendPaymentAMPInvoiceCase(ht *lntest.HarnessTest, // \ / // \__ Dave ____/ // - mppReq := &mppOpenChannelRequest{ - amtAliceCarol: 285000, - amtAliceDave: 155000, - amtCarolBob: 200000, - amtCarolEve: 155000, - amtDaveBob: 155000, - amtEveBob: 155000, - } - mts.openChannels(mppReq) + paymentAmt := mts.setupSendPaymentCase() + chanPointAliceDave := mts.channelPoints[1] chanPointDaveBob := mts.channelPoints[4] @@ -373,7 +364,6 @@ func testSendPaymentAMPInvoiceRepeat(ht *lntest.HarnessTest) { // destination using SendPaymentV2. func testSendPaymentAMP(ht *lntest.HarnessTest) { mts := newMppTestScenario(ht) - const paymentAmt = btcutil.Amount(300000) // Set up a network with three different paths Alice <-> Bob. Channel // capacities are set such that the payment can only succeed if (at @@ -385,15 +375,8 @@ func testSendPaymentAMP(ht *lntest.HarnessTest) { // \ / // \__ Dave ____/ // - mppReq := &mppOpenChannelRequest{ - amtAliceCarol: 285000, - amtAliceDave: 155000, - amtCarolBob: 200000, - amtCarolEve: 155000, - amtDaveBob: 155000, - amtEveBob: 155000, - } - mts.openChannels(mppReq) + paymentAmt := mts.setupSendPaymentCase() + chanPointAliceDave := mts.channelPoints[1] // Increase Dave's fee to make the test deterministic. Otherwise, it @@ -497,12 +480,6 @@ func testSendPaymentAMP(ht *lntest.HarnessTest) { func testSendToRouteAMP(ht *lntest.HarnessTest) { mts := newMppTestScenario(ht) - const ( - paymentAmt = btcutil.Amount(300000) - numShards = 3 - shardAmt = paymentAmt / numShards - chanAmt = shardAmt * 3 / 2 - ) // Subscribe to bob's invoices. req := &lnrpc.InvoiceSubscription{} @@ -515,20 +492,10 @@ func testSendToRouteAMP(ht *lntest.HarnessTest) { // \ / // \__ Dave ____/ // - mppReq := &mppOpenChannelRequest{ - // Since the channel Alice-> Carol will have to carry two - // shards, we make it larger. - amtAliceCarol: chanAmt + shardAmt, - amtAliceDave: chanAmt, - amtCarolBob: chanAmt, - amtCarolEve: chanAmt, - amtDaveBob: chanAmt, - amtEveBob: chanAmt, - } - mts.openChannels(mppReq) + paymentAmt, shardAmt := mts.setupSendToRouteCase() // We'll send shards along three routes from Alice. - sendRoutes := [numShards][]*node.HarnessNode{ + sendRoutes := [][]*node.HarnessNode{ {mts.carol, mts.bob}, {mts.dave, mts.bob}, {mts.carol, mts.eve, mts.bob}, @@ -662,7 +629,7 @@ func testSendToRouteAMP(ht *lntest.HarnessTest) { // Finally, assert that the proper set id is recorded for each htlc, and // that the preimage hash pair is valid. - require.Equal(ht, numShards, len(rpcInvoice.Htlcs)) + require.Equal(ht, 3, len(rpcInvoice.Htlcs)) for _, htlc := range rpcInvoice.Htlcs { require.NotNil(ht, htlc.Amp) require.Equal(ht, setID, htlc.Amp.SetId) diff --git a/itest/lnd_mpp_test.go b/itest/lnd_mpp_test.go index 7bc23da2a8..902f5fec92 100644 --- a/itest/lnd_mpp_test.go +++ b/itest/lnd_mpp_test.go @@ -20,8 +20,6 @@ import ( func testSendMultiPathPayment(ht *lntest.HarnessTest) { mts := newMppTestScenario(ht) - const paymentAmt = btcutil.Amount(300000) - // Set up a network with three different paths Alice <-> Bob. Channel // capacities are set such that the payment can only succeed if (at // least) three paths are used. @@ -32,15 +30,8 @@ func testSendMultiPathPayment(ht *lntest.HarnessTest) { // \ / // \__ Dave ____/ // - req := &mppOpenChannelRequest{ - amtAliceCarol: 285000, - amtAliceDave: 155000, - amtCarolBob: 200000, - amtCarolEve: 155000, - amtDaveBob: 155000, - amtEveBob: 155000, - } - mts.openChannels(req) + paymentAmt := mts.setupSendPaymentCase() + chanPointAliceDave := mts.channelPoints[1] // Increase Dave's fee to make the test deterministic. Otherwise, it @@ -127,11 +118,6 @@ func testSendToRouteMultiPath(ht *lntest.HarnessTest) { // To ensure the payment goes through separate paths, we'll set a // channel size that can only carry one shard at a time. We'll divide // the payment into 3 shards. - const ( - paymentAmt = btcutil.Amount(300000) - shardAmt = paymentAmt / 3 - chanAmt = shardAmt * 3 / 2 - ) // Set up a network with three different paths Alice <-> Bob. // _ Eve _ @@ -140,17 +126,7 @@ func testSendToRouteMultiPath(ht *lntest.HarnessTest) { // \ / // \__ Dave ____/ // - req := &mppOpenChannelRequest{ - // Since the channel Alice-> Carol will have to carry two - // shards, we make it larger. - amtAliceCarol: chanAmt + shardAmt, - amtAliceDave: chanAmt, - amtCarolBob: chanAmt, - amtCarolEve: chanAmt, - amtDaveBob: chanAmt, - amtEveBob: chanAmt, - } - mts.openChannels(req) + paymentAmt, shardAmt := mts.setupSendToRouteCase() // Make Bob create an invoice for Alice to pay. payReqs, rHashes, invoices := ht.CreatePayReqs(mts.bob, paymentAmt, 1) @@ -284,6 +260,9 @@ type mppTestScenario struct { // Alice -- Carol ---- Bob // \ / // \__ Dave ____/ +// +// The scenario is setup in a way that when sending a payment from Alice to +// Bob, (at least) three routes must be tried to succeed. func newMppTestScenario(ht *lntest.HarnessTest) *mppTestScenario { alice := ht.NewNodeWithCoins("Alice", nil) bob := ht.NewNodeWithCoins("Bob", []string{ @@ -351,6 +330,109 @@ type mppOpenChannelRequest struct { amtEveBob btcutil.Amount } +// setupSendPaymentCase opens channels between the nodes for testing the +// `SendPaymentV2` case, where a payment amount of 300,000 sats is used and it +// tests sending three attempts: the first has 150,000 sats, the rest two have +// 75,000 sats. It returns the payment amt. +func (c *mppTestScenario) setupSendPaymentCase() btcutil.Amount { + // To ensure the payment goes through separate paths, we'll set a + // channel size that can only carry one HTLC attempt at a time. We'll + // divide the payment into 3 attempts. + // + // Set the payment amount to be 300,000 sats. When a route cannot be + // found for a given payment amount, we will halven the amount and try + // the pathfinding again, which means we need to see the following + // three attempts to succeed: + // 1. 1st attempt: 150,000 sats. + // 2. 2nd attempt: 75,000 sats. + // 3. 3rd attempt: 75,000 sats. + paymentAmt := btcutil.Amount(300_000) + + // Prepare to open channels between the nodes. Given our expected + // topology, + // + // _ Eve _ + // / \ + // Alice -- Carol ---- Bob + // \ / + // \__ Dave ___/ + // + // There are three routes from Alice to Bob: + // 1. Alice -> Carol -> Bob + // 2. Alice -> Dave -> Bob + // 3. Alice -> Carol -> Eve -> Bob + // We now use hardcoded amounts so it's easier to reason about the + // test. + req := &mppOpenChannelRequest{ + amtAliceCarol: 285_000, + amtAliceDave: 155_000, + amtCarolBob: 200_000, + amtCarolEve: 155_000, + amtDaveBob: 155_000, + amtEveBob: 155_000, + } + + // Given the above setup, the only possible routes to send each of the + // attempts are: + // - 1st attempt(150,000 sats): Alice->Carol->Bob: 200,000 sats. + // - 2nd attempt(75,000 sats): Alice->Dave->Bob: 155,000 sats. + // - 3rd attempt(75,000 sats): Alice->Carol->Eve->Bob: 155,000 sats. + // + // Open the channels as described above. + c.openChannels(req) + + return paymentAmt +} + +// setupSendToRouteCase opens channels between the nodes for testing the +// `SendToRouteV2` case, where a payment amount of 300,000 sats is used and it +// tests sending three attempts each holding 100,000 sats. It returns the +// payment amount and attempt amount. +func (c *mppTestScenario) setupSendToRouteCase() (btcutil.Amount, + btcutil.Amount) { + + // To ensure the payment goes through separate paths, we'll set a + // channel size that can only carry one HTLC attempt at a time. We'll + // divide the payment into 3 attempts, each holding 100,000 sats. + paymentAmt := btcutil.Amount(300_000) + attemptAmt := btcutil.Amount(100_000) + + // Prepare to open channels between the nodes. Given our expected + // topology, + // + // _ Eve _ + // / \ + // Alice -- Carol ---- Bob + // \ / + // \__ Dave ___/ + // + // There are three routes from Alice to Bob: + // 1. Alice -> Carol -> Bob + // 2. Alice -> Dave -> Bob + // 3. Alice -> Carol -> Eve -> Bob + // We now use hardcoded amounts so it's easier to reason about the + // test. + req := &mppOpenChannelRequest{ + amtAliceCarol: 250_000, + amtAliceDave: 150_000, + amtCarolBob: 150_000, + amtCarolEve: 150_000, + amtDaveBob: 150_000, + amtEveBob: 150_000, + } + + // Given the above setup, the only possible routes to send each of the + // attempts are: + // - 1st attempt(100,000 sats): Alice->Carol->Bob: 150,000 sats. + // - 2nd attempt(100,000 sats): Alice->Dave->Bob: 150,000 sats. + // - 3rd attempt(100,000 sats): Alice->Carol->Eve->Bob: 150,000 sats. + // + // Open the channels as described above. + c.openChannels(req) + + return paymentAmt, attemptAmt +} + // openChannels is a helper to open channels that sets up a network topology // with three different paths Alice <-> Bob as following, // From f9c0cd547818f25fbe44aded0946a53c7b45008e Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 21 Nov 2024 14:09:57 +0800 Subject: [PATCH 121/153] itest+lntest: fix flake in MPP-related tests --- itest/lnd_mpp_test.go | 26 ++++++++++++++++++++++++++ lntest/harness_assertion.go | 7 ++++--- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/itest/lnd_mpp_test.go b/itest/lnd_mpp_test.go index 902f5fec92..e84b3701c0 100644 --- a/itest/lnd_mpp_test.go +++ b/itest/lnd_mpp_test.go @@ -378,6 +378,32 @@ func (c *mppTestScenario) setupSendPaymentCase() btcutil.Amount { // - 2nd attempt(75,000 sats): Alice->Dave->Bob: 155,000 sats. // - 3rd attempt(75,000 sats): Alice->Carol->Eve->Bob: 155,000 sats. // + // There is a case where the payment will fail due to the channel + // capacity not being updated in the graph, which has been seen many + // times: + // 1. the 1st attempt (150,000 sats) is sent via + // Alice->Carol->Eve->Bob, after which the capacity in Carol->Eve + // should decrease. + // 2. the 2nd attempt (75,000 sats) is sent via Alice->Carol->Eve->Bob, + // which shouldn't happen because the capacity in Carol->Eve is + // depleted. However, since the HTLCs are sent in parallel, the 2nd + // attempt can be sent before the capacity is updated in the graph. + // 3. if the 2nd attempt succeeds, the 1st attempt will fail and be + // split into two attempts, each holding 75,000 sats. At this point, + // we have three attempts to send, but only two routes are + // available, causing the payment to be failed. + // 4. In addition, with recent fee buffer addition, the attempts will + // fail even earlier without being further split. + // + // To avoid this case, we now increase the channel capacity of the + // route Carol->Eve->Bob such that even the above case happened, we can + // still send the HTLCs. + // + // TODO(yy): we should properly fix this in the router. Atm we only + // perform this hack to unblock the CI. + req.amtEveBob = 285_000 + req.amtCarolEve = 285_000 + // Open the channels as described above. c.openChannels(req) diff --git a/lntest/harness_assertion.go b/lntest/harness_assertion.go index c684941f73..0c14cd9e08 100644 --- a/lntest/harness_assertion.go +++ b/lntest/harness_assertion.go @@ -1513,13 +1513,14 @@ func (h *HarnessTest) AssertNumHTLCsAndStage(hn *node.HarnessNode, lnutils.SpewLogClosure(target.PendingHtlcs)()) } - for i, htlc := range target.PendingHtlcs { + for _, htlc := range target.PendingHtlcs { if htlc.Stage == stage { continue } - return fmt.Errorf("HTLC %d got stage: %v, "+ - "want stage: %v", i, htlc.Stage, stage) + return fmt.Errorf("HTLC %s got stage: %v, "+ + "want stage: %v", htlc.Outpoint, htlc.Stage, + stage) } return nil From d8d2a54c71c197378138029eb1ef8991ba434290 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 22 Nov 2024 23:16:33 +0800 Subject: [PATCH 122/153] lntest: fix flakeness in `openChannelsForNodes` We now make sure the channel participants have heard their private channel when opening channels. --- lntest/harness.go | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/lntest/harness.go b/lntest/harness.go index 1dac9a12d8..82adbd9891 100644 --- a/lntest/harness.go +++ b/lntest/harness.go @@ -2262,12 +2262,27 @@ func (h *HarnessTest) openChannelsForNodes(nodes []*node.HarnessNode, } resp := h.OpenMultiChannelsAsync(reqs) - // Make sure the nodes know each other's channels if they are public. - if !p.Private { + // If the channels are private, make sure the channel participants know + // the relevant channel. + if p.Private { + for i, chanPoint := range resp { + // Get the channel participants - for n channels we + // would have n+1 nodes. + nodeA, nodeB := nodes[i], nodes[i+1] + h.AssertChannelInGraph(nodeA, chanPoint) + h.AssertChannelInGraph(nodeB, chanPoint) + } + } else { + // Make sure the all nodes know all the channels if they are + // public. for _, node := range nodes { for _, chanPoint := range resp { h.AssertChannelInGraph(node, chanPoint) } + + // Make sure every node has updated its cached graph + // about the edges as indicated in `DescribeGraph`. + h.AssertNumEdges(node, len(resp), false) } } From bdfa085cc70844d5d62877d1a2d31c81ede4d67d Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 8 Nov 2024 15:41:14 +0800 Subject: [PATCH 123/153] itest: optimize blocks mined in `testGarbageCollectLinkNodes` There's no need to mine 80ish blocks here. --- itest/lnd_misc_test.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/itest/lnd_misc_test.go b/itest/lnd_misc_test.go index f983b39e91..30dba0a878 100644 --- a/itest/lnd_misc_test.go +++ b/itest/lnd_misc_test.go @@ -10,7 +10,6 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcwallet/wallet" - "github.com/lightningnetwork/lnd/chainreg" "github.com/lightningnetwork/lnd/funding" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lncfg" @@ -506,12 +505,6 @@ func testGarbageCollectLinkNodes(ht *lntest.HarnessTest) { // close the channel instead. ht.ForceCloseChannel(alice, forceCloseChanPoint) - // We'll need to mine some blocks in order to mark the channel fully - // closed. - ht.MineBlocks( - chainreg.DefaultBitcoinTimeLockDelta - defaultCSV, - ) - // Before we test reconnection, we'll ensure that the channel has been // fully cleaned up for both Carol and Alice. ht.AssertNumPendingForceClose(alice, 0) From 98c7a92a56efab4f05812e4ca94fd22e2e0412a8 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 8 Nov 2024 18:56:18 +0800 Subject: [PATCH 124/153] itest: break remote signer into independent cases So the test can run faster. --- itest/list_on_test.go | 5 +- itest/lnd_remote_signer_test.go | 358 +++++++++++++++++++++----------- 2 files changed, 238 insertions(+), 125 deletions(-) diff --git a/itest/list_on_test.go b/itest/list_on_test.go index b3952cad9e..4d4009db4b 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -508,10 +508,6 @@ var allTestCases = []*lntest.TestCase{ Name: "async payments benchmark", TestFunc: testAsyncPayments, }, - { - Name: "remote signer", - TestFunc: testRemoteSigner, - }, { Name: "taproot coop close", TestFunc: testTaprootCoopClose, @@ -687,4 +683,5 @@ func init() { allTestCases = append(allTestCases, multiHopForceCloseTestCases...) allTestCases = append(allTestCases, watchtowerTestCases...) allTestCases = append(allTestCases, psbtFundingTestCases...) + allTestCases = append(allTestCases, remoteSignerTestCases...) } diff --git a/itest/lnd_remote_signer_test.go b/itest/lnd_remote_signer_test.go index bd92ba37c8..6cbda365aa 100644 --- a/itest/lnd_remote_signer_test.go +++ b/itest/lnd_remote_signer_test.go @@ -16,6 +16,59 @@ import ( "github.com/stretchr/testify/require" ) +// remoteSignerTestCases defines a set of test cases to run against the remote +// signer. +var remoteSignerTestCases = []*lntest.TestCase{ + { + Name: "remote signer random seed", + TestFunc: testRemoteSignerRadomSeed, + }, + { + Name: "remote signer account import", + TestFunc: testRemoteSignerAccountImport, + }, + { + Name: "remote signer channel open", + TestFunc: testRemoteSignerChannelOpen, + }, + { + Name: "remote signer funding input types", + TestFunc: testRemoteSignerChannelFundingInputTypes, + }, + { + Name: "remote signer funding async payments", + TestFunc: testRemoteSignerAsyncPayments, + }, + { + Name: "remote signer funding async payments taproot", + TestFunc: testRemoteSignerAsyncPaymentsTaproot, + }, + { + Name: "remote signer shared key", + TestFunc: testRemoteSignerSharedKey, + }, + { + Name: "remote signer bump fee", + TestFunc: testRemoteSignerBumpFee, + }, + { + Name: "remote signer psbt", + TestFunc: testRemoteSignerPSBT, + }, + { + Name: "remote signer sign output raw", + TestFunc: testRemoteSignerSignOutputRaw, + }, + { + Name: "remote signer verify msg", + TestFunc: testRemoteSignerSignVerifyMsg, + }, + { + Name: "remote signer taproot", + TestFunc: testRemoteSignerTaproot, + }, +} + var ( rootKey = "tprv8ZgxMBicQKsPe6jS4vDm2n7s42Q6MpvghUQqMmSKG7bTZvGKtjrcU3" + "PGzMNG37yzxywrcdvgkwrr8eYXJmbwdvUNVT4Ucv7ris4jvA7BUmg" @@ -53,25 +106,115 @@ var ( }} ) -// testRemoteSigner tests that a watch-only wallet can use a remote signing -// wallet to perform any signing or ECDH operations. -func testRemoteSigner(ht *lntest.HarnessTest) { - type testCase struct { - name string - randomSeed bool - sendCoins bool - commitType lnrpc.CommitmentType - fn func(tt *lntest.HarnessTest, - wo, carol *node.HarnessNode) +// remoteSignerTestCase defines a test case for the remote signer test suite. +type remoteSignerTestCase struct { + name string + randomSeed bool + sendCoins bool + commitType lnrpc.CommitmentType + fn func(tt *lntest.HarnessTest, wo, carol *node.HarnessNode) +} + +// prepareRemoteSignerTest prepares a test case for the remote signer test +// suite by creating three nodes. +func prepareRemoteSignerTest(ht *lntest.HarnessTest, tc remoteSignerTestCase) ( + *node.HarnessNode, *node.HarnessNode, *node.HarnessNode) { + + // Signer is our signing node and has the wallet with the full master + // private key. We test that we can create the watch-only wallet from + // the exported accounts but also from a static key to make sure the + // derivation of the account public keys is correct in both cases. + password := []byte("itestpassword") + var ( + signerNodePubKey = nodePubKey + watchOnlyAccounts = deriveCustomScopeAccounts(ht.T) + signer *node.HarnessNode + err error + ) + if !tc.randomSeed { + signer = ht.RestoreNodeWithSeed( + "Signer", nil, password, nil, rootKey, 0, nil, + ) + } else { + signer = ht.NewNode("Signer", nil) + signerNodePubKey = signer.PubKeyStr + + rpcAccts := signer.RPC.ListAccounts( + &walletrpc.ListAccountsRequest{}, + ) + + watchOnlyAccounts, err = walletrpc.AccountsToWatchOnly( + rpcAccts.Accounts, + ) + require.NoError(ht, err) } - subTests := []testCase{{ + var commitArgs []string + if tc.commitType == lnrpc.CommitmentType_SIMPLE_TAPROOT { + commitArgs = lntest.NodeArgsForCommitType( + tc.commitType, + ) + } + + // WatchOnly is the node that has a watch-only wallet and uses the + // Signer node for any operation that requires access to private keys. + watchOnly := ht.NewNodeRemoteSigner( + "WatchOnly", append([]string{ + "--remotesigner.enable", + fmt.Sprintf( + "--remotesigner.rpchost=localhost:%d", + signer.Cfg.RPCPort, + ), + fmt.Sprintf( + "--remotesigner.tlscertpath=%s", + signer.Cfg.TLSCertPath, + ), + fmt.Sprintf( + "--remotesigner.macaroonpath=%s", + signer.Cfg.AdminMacPath, + ), + }, commitArgs...), + password, &lnrpc.WatchOnly{ + MasterKeyBirthdayTimestamp: 0, + MasterKeyFingerprint: nil, + Accounts: watchOnlyAccounts, + }, + ) + + resp := watchOnly.RPC.GetInfo() + require.Equal(ht, signerNodePubKey, resp.IdentityPubkey) + + if tc.sendCoins { + ht.FundCoins(btcutil.SatoshiPerBitcoin, watchOnly) + ht.AssertWalletAccountBalance( + watchOnly, "default", + btcutil.SatoshiPerBitcoin, 0, + ) + } + + carol := ht.NewNode("carol", commitArgs) + ht.EnsureConnected(watchOnly, carol) + + return signer, watchOnly, carol +} + +// testRemoteSignerRadomSeed tests that a watch-only wallet can use a remote +// signing wallet to perform any signing or ECDH operations. +func testRemoteSignerRadomSeed(ht *lntest.HarnessTest) { + tc := remoteSignerTestCase{ name: "random seed", randomSeed: true, fn: func(tt *lntest.HarnessTest, wo, carol *node.HarnessNode) { // Nothing more to test here. }, - }, { + } + + _, watchOnly, carol := prepareRemoteSignerTest(ht, tc) + tc.fn(ht, watchOnly, carol) +} + +func testRemoteSignerAccountImport(ht *lntest.HarnessTest) { + tc := remoteSignerTestCase{ name: "account import", fn: func(tt *lntest.HarnessTest, wo, carol *node.HarnessNode) { runWalletImportAccountScenario( @@ -79,25 +222,53 @@ func testRemoteSigner(ht *lntest.HarnessTest) { carol, wo, ) }, - }, { + } + + _, watchOnly, carol := prepareRemoteSignerTest(ht, tc) + tc.fn(ht, watchOnly, carol) +} + +func testRemoteSignerChannelOpen(ht *lntest.HarnessTest) { + tc := remoteSignerTestCase{ name: "basic channel open close", sendCoins: true, fn: func(tt *lntest.HarnessTest, wo, carol *node.HarnessNode) { runBasicChannelCreationAndUpdates(tt, wo, carol) }, - }, { + } + + _, watchOnly, carol := prepareRemoteSignerTest(ht, tc) + tc.fn(ht, watchOnly, carol) +} + +func testRemoteSignerChannelFundingInputTypes(ht *lntest.HarnessTest) { + tc := remoteSignerTestCase{ name: "channel funding input types", sendCoins: false, fn: func(tt *lntest.HarnessTest, wo, carol *node.HarnessNode) { runChannelFundingInputTypes(tt, carol, wo) }, - }, { + } + + _, watchOnly, carol := prepareRemoteSignerTest(ht, tc) + tc.fn(ht, watchOnly, carol) +} + +func testRemoteSignerAsyncPayments(ht *lntest.HarnessTest) { + tc := remoteSignerTestCase{ name: "async payments", sendCoins: true, fn: func(tt *lntest.HarnessTest, wo, carol *node.HarnessNode) { runAsyncPayments(tt, wo, carol, nil) }, - }, { + } + + _, watchOnly, carol := prepareRemoteSignerTest(ht, tc) + tc.fn(ht, watchOnly, carol) +} + +func testRemoteSignerAsyncPaymentsTaproot(ht *lntest.HarnessTest) { + tc := remoteSignerTestCase{ name: "async payments taproot", sendCoins: true, fn: func(tt *lntest.HarnessTest, wo, carol *node.HarnessNode) { @@ -108,18 +279,39 @@ func testRemoteSigner(ht *lntest.HarnessTest) { ) }, commitType: lnrpc.CommitmentType_SIMPLE_TAPROOT, - }, { + } + + _, watchOnly, carol := prepareRemoteSignerTest(ht, tc) + tc.fn(ht, watchOnly, carol) +} + +func testRemoteSignerSharedKey(ht *lntest.HarnessTest) { + tc := remoteSignerTestCase{ name: "shared key", fn: func(tt *lntest.HarnessTest, wo, carol *node.HarnessNode) { runDeriveSharedKey(tt, wo) }, - }, { + } + + _, watchOnly, carol := prepareRemoteSignerTest(ht, tc) + tc.fn(ht, watchOnly, carol) +} + +func testRemoteSignerBumpFee(ht *lntest.HarnessTest) { + tc := remoteSignerTestCase{ name: "bumpfee", sendCoins: true, fn: func(tt *lntest.HarnessTest, wo, carol *node.HarnessNode) { runBumpFee(tt, wo) }, - }, { + } + + _, watchOnly, carol := prepareRemoteSignerTest(ht, tc) + tc.fn(ht, watchOnly, carol) +} + +func testRemoteSignerPSBT(ht *lntest.HarnessTest) { + tc := remoteSignerTestCase{ name: "psbt", randomSeed: true, fn: func(tt *lntest.HarnessTest, wo, carol *node.HarnessNode) { @@ -137,19 +329,40 @@ func testRemoteSigner(ht *lntest.HarnessTest) { // sure we can fund and then sign PSBTs from our wallet. runFundAndSignPsbt(ht, wo) }, - }, { + } + + _, watchOnly, carol := prepareRemoteSignerTest(ht, tc) + tc.fn(ht, watchOnly, carol) +} + +func testRemoteSignerSignOutputRaw(ht *lntest.HarnessTest) { + tc := remoteSignerTestCase{ name: "sign output raw", sendCoins: true, fn: func(tt *lntest.HarnessTest, wo, carol *node.HarnessNode) { runSignOutputRaw(tt, wo) }, - }, { + } + + _, watchOnly, carol := prepareRemoteSignerTest(ht, tc) + tc.fn(ht, watchOnly, carol) +} + +func testRemoteSignerSignVerifyMsg(ht *lntest.HarnessTest) { + tc := remoteSignerTestCase{ name: "sign verify msg", sendCoins: true, fn: func(tt *lntest.HarnessTest, wo, carol *node.HarnessNode) { runSignVerifyMessage(tt, wo) }, - }, { + } + + _, watchOnly, carol := prepareRemoteSignerTest(ht, tc) + tc.fn(ht, watchOnly, carol) +} + +func testRemoteSignerTaproot(ht *lntest.HarnessTest) { + tc := remoteSignerTestCase{ name: "taproot", sendCoins: true, randomSeed: true, @@ -175,107 +388,10 @@ func testRemoteSigner(ht *lntest.HarnessTest) { ) } }, - }} - - prepareTest := func(st *lntest.HarnessTest, - subTest testCase) (*node.HarnessNode, - *node.HarnessNode, *node.HarnessNode) { - - // Signer is our signing node and has the wallet with the full - // master private key. We test that we can create the watch-only - // wallet from the exported accounts but also from a static key - // to make sure the derivation of the account public keys is - // correct in both cases. - password := []byte("itestpassword") - var ( - signerNodePubKey = nodePubKey - watchOnlyAccounts = deriveCustomScopeAccounts(ht.T) - signer *node.HarnessNode - err error - ) - if !subTest.randomSeed { - signer = st.RestoreNodeWithSeed( - "Signer", nil, password, nil, rootKey, 0, nil, - ) - } else { - signer = st.NewNode("Signer", nil) - signerNodePubKey = signer.PubKeyStr - - rpcAccts := signer.RPC.ListAccounts( - &walletrpc.ListAccountsRequest{}, - ) - - watchOnlyAccounts, err = walletrpc.AccountsToWatchOnly( - rpcAccts.Accounts, - ) - require.NoError(st, err) - } - - var commitArgs []string - if subTest.commitType == lnrpc.CommitmentType_SIMPLE_TAPROOT { - commitArgs = lntest.NodeArgsForCommitType( - subTest.commitType, - ) - } - - // WatchOnly is the node that has a watch-only wallet and uses - // the Signer node for any operation that requires access to - // private keys. - watchOnly := st.NewNodeRemoteSigner( - "WatchOnly", append([]string{ - "--remotesigner.enable", - fmt.Sprintf( - "--remotesigner.rpchost=localhost:%d", - signer.Cfg.RPCPort, - ), - fmt.Sprintf( - "--remotesigner.tlscertpath=%s", - signer.Cfg.TLSCertPath, - ), - fmt.Sprintf( - "--remotesigner.macaroonpath=%s", - signer.Cfg.AdminMacPath, - ), - }, commitArgs...), - password, &lnrpc.WatchOnly{ - MasterKeyBirthdayTimestamp: 0, - MasterKeyFingerprint: nil, - Accounts: watchOnlyAccounts, - }, - ) - - resp := watchOnly.RPC.GetInfo() - require.Equal(st, signerNodePubKey, resp.IdentityPubkey) - - if subTest.sendCoins { - st.FundCoins(btcutil.SatoshiPerBitcoin, watchOnly) - ht.AssertWalletAccountBalance( - watchOnly, "default", - btcutil.SatoshiPerBitcoin, 0, - ) - } - - carol := st.NewNode("carol", commitArgs) - st.EnsureConnected(watchOnly, carol) - - return signer, watchOnly, carol } - for _, testCase := range subTests { - subTest := testCase - - success := ht.Run(subTest.name, func(tt *testing.T) { - // Skip the cleanup here as no standby node is used. - st := ht.Subtest(tt) - - _, watchOnly, carol := prepareTest(st, subTest) - subTest.fn(st, watchOnly, carol) - }) - - if !success { - return - } - } + _, watchOnly, carol := prepareRemoteSignerTest(ht, tc) + tc.fn(ht, watchOnly, carol) } // deriveCustomScopeAccounts derives the first 255 default accounts of the custom lnd From 136c6f3f913cf4d782200f846229fe058e1a574e Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 8 Nov 2024 19:10:02 +0800 Subject: [PATCH 125/153] itest: break down channel restore commit types cases --- itest/list_on_test.go | 5 +- itest/lnd_channel_backup_test.go | 137 +++++++++++++++---------------- 2 files changed, 65 insertions(+), 77 deletions(-) diff --git a/itest/list_on_test.go b/itest/list_on_test.go index 4d4009db4b..66a7a4ff38 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -27,10 +27,6 @@ var allTestCases = []*lntest.TestCase{ Name: "channel backup restore unconfirmed", TestFunc: testChannelBackupRestoreUnconfirmed, }, - { - Name: "channel backup restore commit types", - TestFunc: testChannelBackupRestoreCommitTypes, - }, { Name: "channel backup restore force close", TestFunc: testChannelBackupRestoreForceClose, @@ -684,4 +680,5 @@ func init() { allTestCases = append(allTestCases, watchtowerTestCases...) allTestCases = append(allTestCases, psbtFundingTestCases...) allTestCases = append(allTestCases, remoteSignerTestCases...) + allTestCases = append(allTestCases, channelRestoreTestCases...) } diff --git a/itest/lnd_channel_backup_test.go b/itest/lnd_channel_backup_test.go index d96a44d0a4..2a85501cf7 100644 --- a/itest/lnd_channel_backup_test.go +++ b/itest/lnd_channel_backup_test.go @@ -23,6 +23,70 @@ import ( "github.com/stretchr/testify/require" ) +// channelRestoreTestCases contains the test cases for the channel restore +// scenario. +var channelRestoreTestCases = []*lntest.TestCase{ + { + // Restore the backup from the on-disk file, using the RPC + // interface, for anchor commitment channels. + Name: "channel backup restore anchor", + TestFunc: func(ht *lntest.HarnessTest) { + runChanRestoreScenarioCommitTypes( + ht, lnrpc.CommitmentType_ANCHORS, false, + ) + }, + }, + { + // Restore the backup from the on-disk file, using the RPC + // interface, for script-enforced leased channels. + Name: "channel backup restore leased", + TestFunc: func(ht *lntest.HarnessTest) { + runChanRestoreScenarioCommitTypes( + ht, leasedType, false, + ) + }, + }, + { + // Restore the backup from the on-disk file, using the RPC + // interface, for zero-conf anchor channels. + Name: "channel backup restore anchor zero conf", + TestFunc: func(ht *lntest.HarnessTest) { + runChanRestoreScenarioCommitTypes( + ht, lnrpc.CommitmentType_ANCHORS, true, + ) + }, + }, + { + // Restore the backup from the on-disk file, using the RPC + // interface for a zero-conf script-enforced leased channel. + Name: "channel backup restore leased zero conf", + TestFunc: func(ht *lntest.HarnessTest) { + runChanRestoreScenarioCommitTypes( + ht, leasedType, true, + ) + }, + }, + { + // Restore a channel back up of a taproot channel that was + // confirmed. + Name: "channel backup restore simple taproot", + TestFunc: func(ht *lntest.HarnessTest) { + runChanRestoreScenarioCommitTypes( + ht, lnrpc.CommitmentType_SIMPLE_TAPROOT, false, + ) + }, + }, + { + // Restore a channel back up of an unconfirmed taproot channel. + Name: "channel backup restore simple taproot zero conf", + TestFunc: func(ht *lntest.HarnessTest) { + runChanRestoreScenarioCommitTypes( + ht, lnrpc.CommitmentType_SIMPLE_TAPROOT, true, + ) + }, + }, +} + type ( // nodeRestorer is a function closure that allows each test case to // control exactly *how* the prior node is restored. This might be @@ -540,79 +604,6 @@ func runChanRestoreScenarioUnConfirmed(ht *lntest.HarnessTest, useFile bool) { crs.testScenario(ht, restoredNodeFunc) } -// testChannelBackupRestoreCommitTypes tests that we're able to recover from, -// and initiate the DLP protocol for different channel commitment types and -// zero-conf channel. -func testChannelBackupRestoreCommitTypes(ht *lntest.HarnessTest) { - var testCases = []struct { - name string - ct lnrpc.CommitmentType - zeroConf bool - }{ - // Restore the backup from the on-disk file, using the RPC - // interface, for anchor commitment channels. - { - name: "restore from backup file anchors", - ct: lnrpc.CommitmentType_ANCHORS, - }, - - // Restore the backup from the on-disk file, using the RPC - // interface, for script-enforced leased channels. - { - name: "restore from backup file script " + - "enforced lease", - ct: lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE, - }, - - // Restore the backup from the on-disk file, using the RPC - // interface, for zero-conf anchor channels. - { - name: "restore from backup file for zero-conf " + - "anchors channel", - ct: lnrpc.CommitmentType_ANCHORS, - zeroConf: true, - }, - - // Restore the backup from the on-disk file, using the RPC - // interface for a zero-conf script-enforced leased channel. - { - name: "restore from backup file zero-conf " + - "script-enforced leased channel", - ct: lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE, - zeroConf: true, - }, - - // Restore a channel back up of a taproot channel that was - // confirmed. - { - name: "restore from backup taproot", - ct: lnrpc.CommitmentType_SIMPLE_TAPROOT, - zeroConf: false, - }, - - // Restore a channel back up of an unconfirmed taproot channel. - { - name: "restore from backup taproot zero conf", - ct: lnrpc.CommitmentType_SIMPLE_TAPROOT, - zeroConf: true, - }, - } - - for _, testCase := range testCases { - tc := testCase - success := ht.Run(tc.name, func(t *testing.T) { - h := ht.Subtest(t) - - runChanRestoreScenarioCommitTypes( - h, tc.ct, tc.zeroConf, - ) - }) - if !success { - break - } - } -} - // runChanRestoreScenarioCommitTypes tests that the DLP is applied for // different channel commitment types and zero-conf channel. func runChanRestoreScenarioCommitTypes(ht *lntest.HarnessTest, From a3473eca55475e266f30eee7b16aad77d3d637e0 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 8 Nov 2024 20:39:44 +0800 Subject: [PATCH 126/153] itest: break down utxo selection funding tests --- itest/list_on_test.go | 5 +- ...lnd_channel_funding_utxo_selection_test.go | 405 +++++++++++++----- 2 files changed, 287 insertions(+), 123 deletions(-) diff --git a/itest/list_on_test.go b/itest/list_on_test.go index 66a7a4ff38..d51afc05b9 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -556,10 +556,6 @@ var allTestCases = []*lntest.TestCase{ Name: "custom features", TestFunc: testCustomFeatures, }, - { - Name: "utxo selection funding", - TestFunc: testChannelUtxoSelection, - }, { Name: "update pending open channels on funder side", TestFunc: testUpdateOnFunderPendingOpenChannels, @@ -681,4 +677,5 @@ func init() { allTestCases = append(allTestCases, psbtFundingTestCases...) allTestCases = append(allTestCases, remoteSignerTestCases...) allTestCases = append(allTestCases, channelRestoreTestCases...) + allTestCases = append(allTestCases, fundUtxoSelectionTestCases...) } diff --git a/itest/lnd_channel_funding_utxo_selection_test.go b/itest/lnd_channel_funding_utxo_selection_test.go index f840b8ac99..7868b73338 100644 --- a/itest/lnd_channel_funding_utxo_selection_test.go +++ b/itest/lnd_channel_funding_utxo_selection_test.go @@ -15,6 +15,37 @@ import ( "github.com/stretchr/testify/require" ) +var fundUtxoSelectionTestCases = []*lntest.TestCase{ + { + Name: "utxo selection funding error", + TestFunc: testChannelUtxoSelectionError, + }, + { + Name: "utxo selection selected valid chan size", + TestFunc: testUtxoSelectionSelectedValidChanSize, + }, + { + Name: "utxo selection selected valid chan reserve", + TestFunc: testUtxoSelectionSelectedValidChanReserve, + }, + { + Name: "utxo selection selected reserve from selected", + TestFunc: testUtxoSelectionReserveFromSelected, + }, + { + Name: "utxo selection fundmax", + TestFunc: testUtxoSelectionFundmax, + }, + { + Name: "utxo selection fundmax reserve", + TestFunc: testUtxoSelectionFundmaxReserve, + }, + { + Name: "utxo selection reused utxo", + TestFunc: testUtxoSelectionReuseUTXO, + }, +} + type chanFundUtxoSelectionTestCase struct { // name is the name of the target test case. name string @@ -57,9 +88,10 @@ type chanFundUtxoSelectionTestCase struct { reuseUtxo bool } -// testChannelUtxoSelection checks various channel funding scenarios where the -// user instructed the wallet to use a selection funds available in the wallet. -func testChannelUtxoSelection(ht *lntest.HarnessTest) { +// testChannelUtxoSelectionError checks various channel funding error scenarios +// where the user instructed the wallet to use a selection funds available in +// the wallet. +func testChannelUtxoSelectionError(ht *lntest.HarnessTest) { // Create two new nodes that open a channel between each other for these // tests. args := lntest.NodeArgsForCommitType(lnrpc.CommitmentType_ANCHORS) @@ -115,73 +147,6 @@ func testChannelUtxoSelection(ht *lntest.HarnessTest) { "create funding transaction, need 0.00210337 " + "BTC only have 0.00100000 BTC available", }, - // We are spending two selected coins partially out of three - // available in the wallet and expect a change output and the - // unselected coin as remaining wallet balance. - { - name: "selected, local amount > " + - "min chan size", - initialCoins: []btcutil.Amount{ - 200_000, 50_000, 100_000, - }, - selectedCoins: []btcutil.Amount{ - 200_000, 100_000, - }, - localAmt: btcutil.Amount(250_000), - expectedBalance: btcutil.Amount(250_000), - remainingWalletBalance: btcutil.Amount(350_000) - - btcutil.Amount(250_000) - fundingFee(2, true), - }, - // We are spending the entirety of two selected coins out of - // three available in the wallet and expect no change output and - // the unselected coin as remaining wallet balance. - { - name: "fundmax, local amount > min " + - "chan size", - initialCoins: []btcutil.Amount{ - 200_000, 100_000, 50_000, - }, - selectedCoins: []btcutil.Amount{ - 200_000, 50_000, - }, - expectedBalance: btcutil.Amount(200_000) + - btcutil.Amount(50_000) - fundingFee(2, false), - remainingWalletBalance: btcutil.Amount(100_000), - }, - // Select all coins in wallet and use the maximum available - // local amount to fund an anchor channel. - { - name: "selected, local amount leaves sufficient " + - "reserve", - initialCoins: []btcutil.Amount{ - 200_000, 100_000, - }, - selectedCoins: []btcutil.Amount{200_000, 100_000}, - commitmentType: lnrpc.CommitmentType_ANCHORS, - localAmt: btcutil.Amount(300_000) - - reserveAmount - fundingFee(2, true), - expectedBalance: btcutil.Amount(300_000) - - reserveAmount - fundingFee(2, true), - remainingWalletBalance: reserveAmount, - }, - // Select all coins in wallet towards local amount except for an - // anchor reserve portion. Because the UTXOs are sorted by size - // by default, the reserve amount is just left in the wallet. - { - name: "selected, reserve from selected", - initialCoins: []btcutil.Amount{ - 200_000, reserveAmount, 100_000, - }, - selectedCoins: []btcutil.Amount{ - 200_000, reserveAmount, 100_000, - }, - commitmentType: lnrpc.CommitmentType_ANCHORS, - localAmt: btcutil.Amount(300_000) - - fundingFee(2, true), - expectedBalance: btcutil.Amount(300_000) - - fundingFee(2, true), - remainingWalletBalance: reserveAmount, - }, // Select all coins in wallet and use more than the maximum // available local amount to fund an anchor channel. { @@ -200,43 +165,6 @@ func testChannelUtxoSelection(ht *lntest.HarnessTest) { "insufficient funds for fee bumping anchor " + "channel closings", }, - // We fund an anchor channel with a single coin and just keep - // enough funds in the wallet to cover for the anchor reserve. - { - name: "fundmax, sufficient reserve", - initialCoins: []btcutil.Amount{ - 200_000, reserveAmount, - }, - selectedCoins: []btcutil.Amount{200_000}, - commitmentType: lnrpc.CommitmentType_ANCHORS, - expectedBalance: btcutil.Amount(200_000) - - fundingFee(1, false), - remainingWalletBalance: reserveAmount, - }, - // We fund an anchor channel with a single coin and expect the - // reserve amount left in the wallet. - { - name: "fundmax, sufficient reserve from channel " + - "balance carve out", - initialCoins: []btcutil.Amount{ - 200_000, - }, - selectedCoins: []btcutil.Amount{200_000}, - commitmentType: lnrpc.CommitmentType_ANCHORS, - expectedBalance: btcutil.Amount(200_000) - - reserveAmount - fundingFee(1, true), - remainingWalletBalance: reserveAmount, - }, - // Confirm that already spent outputs can't be reused to fund - // another channel. - { - name: "output already spent", - initialCoins: []btcutil.Amount{ - 200_000, - }, - selectedCoins: []btcutil.Amount{200_000}, - reuseUtxo: true, - }, } for _, tc := range tcs { @@ -255,24 +183,258 @@ func testChannelUtxoSelection(ht *lntest.HarnessTest) { } } +// testChannelUtxoSelection checks various channel funding scenarios where the +// user instructed the wallet to use a selection funds available in the wallet. +func testUtxoSelectionSelectedValidChanSize(ht *lntest.HarnessTest) { + // Create two new nodes that open a channel between each other for these + // tests. + args := lntest.NodeArgsForCommitType(lnrpc.CommitmentType_ANCHORS) + alice := ht.NewNode("Alice", args) + bob := ht.NewNode("Bob", args) + + // Ensure both sides are connected so the funding flow can be properly + // executed. + ht.EnsureConnected(alice, bob) + + // Calculate reserve amount for one channel. + reserveResp, _ := alice.RPC.WalletKit.RequiredReserve( + context.Background(), &walletrpc.RequiredReserveRequest{ + AdditionalPublicChannels: 1, + }, + ) + + reserveAmount := btcutil.Amount(reserveResp.RequiredReserve) + + // We are spending two selected coins partially out of three available + // in the wallet and expect a change output and the unselected coin as + // remaining wallet balance. + tc := &chanFundUtxoSelectionTestCase{ + name: "selected, local amount > min chan size", + initialCoins: []btcutil.Amount{ + 200_000, 50_000, 100_000, + }, + selectedCoins: []btcutil.Amount{ + 200_000, 100_000, + }, + localAmt: btcutil.Amount(250_000), + expectedBalance: btcutil.Amount(250_000), + remainingWalletBalance: btcutil.Amount(350_000) - + btcutil.Amount(250_000) - fundingFee(2, true), + } + + runUtxoSelectionTestCase(ht, alice, bob, tc, reserveAmount) +} + +// testChannelUtxoSelection checks various channel funding scenarios where the +// user instructed the wallet to use a selection funds available in the wallet. +func testUtxoSelectionSelectedValidChanReserve(ht *lntest.HarnessTest) { + // Create two new nodes that open a channel between each other for these + // tests. + args := lntest.NodeArgsForCommitType(lnrpc.CommitmentType_ANCHORS) + alice := ht.NewNode("Alice", args) + bob := ht.NewNode("Bob", args) + + // Ensure both sides are connected so the funding flow can be properly + // executed. + ht.EnsureConnected(alice, bob) + + // Calculate reserve amount for one channel. + reserveResp, _ := alice.RPC.WalletKit.RequiredReserve( + context.Background(), &walletrpc.RequiredReserveRequest{ + AdditionalPublicChannels: 1, + }, + ) + + reserveAmount := btcutil.Amount(reserveResp.RequiredReserve) + + // Select all coins in wallet and use the maximum available + // local amount to fund an anchor channel. + tc := &chanFundUtxoSelectionTestCase{ + name: "selected, local amount leaves sufficient reserve", + initialCoins: []btcutil.Amount{ + 200_000, 100_000, + }, + selectedCoins: []btcutil.Amount{200_000, 100_000}, + commitmentType: lnrpc.CommitmentType_ANCHORS, + localAmt: btcutil.Amount(300_000) - + reserveAmount - fundingFee(2, true), + expectedBalance: btcutil.Amount(300_000) - + reserveAmount - fundingFee(2, true), + remainingWalletBalance: reserveAmount, + } + + runUtxoSelectionTestCase(ht, alice, bob, tc, reserveAmount) +} + +// testChannelUtxoSelection checks various channel funding scenarios where the +// user instructed the wallet to use a selection funds available in the wallet. +func testUtxoSelectionReserveFromSelected(ht *lntest.HarnessTest) { + // Create two new nodes that open a channel between each other for these + // tests. + args := lntest.NodeArgsForCommitType(lnrpc.CommitmentType_ANCHORS) + alice := ht.NewNode("Alice", args) + bob := ht.NewNode("Bob", args) + + // Ensure both sides are connected so the funding flow can be properly + // executed. + ht.EnsureConnected(alice, bob) + + // Calculate reserve amount for one channel. + reserveResp, _ := alice.RPC.WalletKit.RequiredReserve( + context.Background(), &walletrpc.RequiredReserveRequest{ + AdditionalPublicChannels: 1, + }, + ) + + reserveAmount := btcutil.Amount(reserveResp.RequiredReserve) + + // Select all coins in wallet towards local amount except for an anchor + // reserve portion. Because the UTXOs are sorted by size by default, + // the reserve amount is just left in the wallet. + tc := &chanFundUtxoSelectionTestCase{ + name: "selected, reserve from selected", + initialCoins: []btcutil.Amount{ + 200_000, reserveAmount, 100_000, + }, + selectedCoins: []btcutil.Amount{ + 200_000, reserveAmount, 100_000, + }, + commitmentType: lnrpc.CommitmentType_ANCHORS, + localAmt: btcutil.Amount(300_000) - + fundingFee(2, true), + expectedBalance: btcutil.Amount(300_000) - + fundingFee(2, true), + remainingWalletBalance: reserveAmount, + } + + runUtxoSelectionTestCase(ht, alice, bob, tc, reserveAmount) +} + +// testChannelUtxoSelection checks various channel funding scenarios where the +// user instructed the wallet to use a selection funds available in the wallet. +func testUtxoSelectionFundmax(ht *lntest.HarnessTest) { + // Create two new nodes that open a channel between each other for these + // tests. + args := lntest.NodeArgsForCommitType(lnrpc.CommitmentType_ANCHORS) + alice := ht.NewNode("Alice", args) + bob := ht.NewNode("Bob", args) + + // Ensure both sides are connected so the funding flow can be properly + // executed. + ht.EnsureConnected(alice, bob) + + // Calculate reserve amount for one channel. + reserveResp, _ := alice.RPC.WalletKit.RequiredReserve( + context.Background(), &walletrpc.RequiredReserveRequest{ + AdditionalPublicChannels: 1, + }, + ) + + reserveAmount := btcutil.Amount(reserveResp.RequiredReserve) + + // We fund an anchor channel with a single coin and just keep enough + // funds in the wallet to cover for the anchor reserve. + tc := &chanFundUtxoSelectionTestCase{ + name: "fundmax, sufficient reserve", + initialCoins: []btcutil.Amount{ + 200_000, reserveAmount, + }, + selectedCoins: []btcutil.Amount{200_000}, + commitmentType: lnrpc.CommitmentType_ANCHORS, + expectedBalance: btcutil.Amount(200_000) - + fundingFee(1, false), + remainingWalletBalance: reserveAmount, + } + + runUtxoSelectionTestCase(ht, alice, bob, tc, reserveAmount) +} + +// testChannelUtxoSelection checks various channel funding scenarios where the +// user instructed the wallet to use a selection funds available in the wallet. +func testUtxoSelectionFundmaxReserve(ht *lntest.HarnessTest) { + // Create two new nodes that open a channel between each other for these + // tests. + args := lntest.NodeArgsForCommitType(lnrpc.CommitmentType_ANCHORS) + alice := ht.NewNode("Alice", args) + bob := ht.NewNode("Bob", args) + + // Ensure both sides are connected so the funding flow can be properly + // executed. + ht.EnsureConnected(alice, bob) + + // Calculate reserve amount for one channel. + reserveResp, _ := alice.RPC.WalletKit.RequiredReserve( + context.Background(), &walletrpc.RequiredReserveRequest{ + AdditionalPublicChannels: 1, + }, + ) + + reserveAmount := btcutil.Amount(reserveResp.RequiredReserve) + + // We fund an anchor channel with a single coin and expect the reserve + // amount left in the wallet. + tc := &chanFundUtxoSelectionTestCase{ + name: "fundmax, sufficient reserve from channel " + + "balance carve out", + initialCoins: []btcutil.Amount{ + 200_000, + }, + selectedCoins: []btcutil.Amount{200_000}, + commitmentType: lnrpc.CommitmentType_ANCHORS, + expectedBalance: btcutil.Amount(200_000) - + reserveAmount - fundingFee(1, true), + remainingWalletBalance: reserveAmount, + } + + runUtxoSelectionTestCase(ht, alice, bob, tc, reserveAmount) +} + +// testChannelUtxoSelection checks various channel funding scenarios where the +// user instructed the wallet to use a selection funds available in the wallet. +func testUtxoSelectionReuseUTXO(ht *lntest.HarnessTest) { + // Create two new nodes that open a channel between each other for these + // tests. + args := lntest.NodeArgsForCommitType(lnrpc.CommitmentType_ANCHORS) + alice := ht.NewNode("Alice", args) + bob := ht.NewNode("Bob", args) + + // Ensure both sides are connected so the funding flow can be properly + // executed. + ht.EnsureConnected(alice, bob) + + // Calculate reserve amount for one channel. + reserveResp, _ := alice.RPC.WalletKit.RequiredReserve( + context.Background(), &walletrpc.RequiredReserveRequest{ + AdditionalPublicChannels: 1, + }, + ) + + reserveAmount := btcutil.Amount(reserveResp.RequiredReserve) + + // Confirm that already spent outputs can't be reused to fund another + // channel. + tc := &chanFundUtxoSelectionTestCase{ + name: "output already spent", + initialCoins: []btcutil.Amount{ + 200_000, + }, + selectedCoins: []btcutil.Amount{200_000}, + reuseUtxo: true, + } + + runUtxoSelectionTestCase(ht, alice, bob, tc, reserveAmount) +} + // runUtxoSelectionTestCase runs a single test case asserting that test // conditions are met. func runUtxoSelectionTestCase(ht *lntest.HarnessTest, alice, bob *node.HarnessNode, tc *chanFundUtxoSelectionTestCase, reserveAmount btcutil.Amount) { - // fund initial coins + // Fund initial coins. for _, initialCoin := range tc.initialCoins { ht.FundCoins(initialCoin, alice) } - defer func() { - // Fund additional coins to sweep in case the wallet contains - // dust. - ht.FundCoins(100_000, alice) - - // Remove all funds from Alice. - sweepNodeWalletAndAssert(ht, alice) - }() // Create an outpoint lookup for each unique amount. lookup := make(map[int64]*lnrpc.OutPoint) @@ -314,9 +476,14 @@ func runUtxoSelectionTestCase(ht *lntest.HarnessTest, alice, // successful, simply check for an error. if tc.chanOpenShouldFail { expectedErr := errors.New(tc.expectedErrStr) - ht.OpenChannelAssertErr( - alice, bob, chanParams, expectedErr, - ) + ht.OpenChannelAssertErr(alice, bob, chanParams, expectedErr) + + // Fund additional coins to sweep in case the wallet contains + // dust. + ht.FundCoins(100_000, alice) + + // Remove all funds from Alice. + sweepNodeWalletAndAssert(ht, alice) return } From db84d567ad45705b1dfb1407f7c20d48a2d6268e Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 8 Nov 2024 22:30:57 +0800 Subject: [PATCH 127/153] itest: break all multihop test cases --- itest/lnd_multi-hop_force_close_test.go | 1486 +++++++++++------------ 1 file changed, 717 insertions(+), 769 deletions(-) diff --git a/itest/lnd_multi-hop_force_close_test.go b/itest/lnd_multi-hop_force_close_test.go index 308fa9be05..22a4803198 100644 --- a/itest/lnd_multi-hop_force_close_test.go +++ b/itest/lnd_multi-hop_force_close_test.go @@ -1,8 +1,6 @@ package itest import ( - "testing" - "github.com/btcsuite/btcd/btcutil" "github.com/lightningnetwork/lnd/lncfg" "github.com/lightningnetwork/lnd/lnrpc" @@ -34,130 +32,207 @@ var multiHopForceCloseTestCases = []*lntest.TestCase{ Name: "multihop local claim outgoing htlc anchor", TestFunc: testLocalClaimOutgoingHTLCAnchor, }, + { + Name: "multihop local claim outgoing htlc anchor zero conf", + TestFunc: testLocalClaimOutgoingHTLCAnchorZeroConf, + }, { Name: "multihop local claim outgoing htlc simple taproot", TestFunc: testLocalClaimOutgoingHTLCSimpleTaproot, }, + { + Name: "multihop local claim outgoing htlc simple taproot zero conf", + TestFunc: testLocalClaimOutgoingHTLCSimpleTaprootZeroConf, + }, { Name: "multihop local claim outgoing htlc leased", TestFunc: testLocalClaimOutgoingHTLCLeased, }, + { + Name: "multihop local claim outgoing htlc leased zero conf", + TestFunc: testLocalClaimOutgoingHTLCLeasedZeroConf, + }, { Name: "multihop receiver preimage claim anchor", TestFunc: testMultiHopReceiverPreimageClaimAnchor, }, + { + Name: "multihop receiver preimage claim anchor zero conf", + TestFunc: testMultiHopReceiverPreimageClaimAnchorZeroConf, + }, { Name: "multihop receiver preimage claim simple taproot", TestFunc: testMultiHopReceiverPreimageClaimSimpleTaproot, }, + { + Name: "multihop receiver preimage claim simple taproot zero conf", + TestFunc: testMultiHopReceiverPreimageClaimSimpleTaprootZeroConf, + }, { Name: "multihop receiver preimage claim leased", TestFunc: testMultiHopReceiverPreimageClaimLeased, }, + { + Name: "multihop receiver preimage claim leased zero conf", + TestFunc: testMultiHopReceiverPreimageClaimLeasedZeroConf, + }, { Name: "multihop local force close before timeout anchor", TestFunc: testLocalForceCloseBeforeTimeoutAnchor, }, + { + Name: "multihop local force close before timeout anchor zero conf", + TestFunc: testLocalForceCloseBeforeTimeoutAnchorZeroConf, + }, { Name: "multihop local force close before timeout simple taproot", TestFunc: testLocalForceCloseBeforeTimeoutSimpleTaproot, }, + { + Name: "multihop local force close before timeout simple taproot zero conf", + TestFunc: testLocalForceCloseBeforeTimeoutSimpleTaprootZeroConf, + }, { Name: "multihop local force close before timeout leased", TestFunc: testLocalForceCloseBeforeTimeoutLeased, }, + { + Name: "multihop local force close before timeout leased zero conf", + TestFunc: testLocalForceCloseBeforeTimeoutLeasedZeroConf, + }, { Name: "multihop remote force close before timeout anchor", TestFunc: testRemoteForceCloseBeforeTimeoutAnchor, }, + { + Name: "multihop remote force close before timeout anchor zero conf", + TestFunc: testRemoteForceCloseBeforeTimeoutAnchorZeroConf, + }, { Name: "multihop remote force close before timeout simple taproot", TestFunc: testRemoteForceCloseBeforeTimeoutSimpleTaproot, }, + { + Name: "multihop remote force close before timeout simple taproot zero conf", + TestFunc: testRemoteForceCloseBeforeTimeoutSimpleTaprootZeroConf, + }, { Name: "multihop remote force close before timeout leased", TestFunc: testRemoteForceCloseBeforeTimeoutLeased, }, + { + Name: "multihop remote force close before timeout leased zero conf", + TestFunc: testRemoteForceCloseBeforeTimeoutLeasedZeroConf, + }, { Name: "multihop local claim incoming htlc anchor", TestFunc: testLocalClaimIncomingHTLCAnchor, }, + { + Name: "multihop local claim incoming htlc anchor zero conf", + TestFunc: testLocalClaimIncomingHTLCAnchorZeroConf, + }, { Name: "multihop local claim incoming htlc simple taproot", TestFunc: testLocalClaimIncomingHTLCSimpleTaproot, }, + { + Name: "multihop local claim incoming htlc simple taproot zero conf", + TestFunc: testLocalClaimIncomingHTLCSimpleTaprootZeroConf, + }, { Name: "multihop local claim incoming htlc leased", TestFunc: testLocalClaimIncomingHTLCLeased, }, + { + Name: "multihop local claim incoming htlc leased zero conf", + TestFunc: testLocalClaimIncomingHTLCLeasedZeroConf, + }, { Name: "multihop local preimage claim anchor", TestFunc: testLocalPreimageClaimAnchor, }, + { + Name: "multihop local preimage claim anchor zero conf", + TestFunc: testLocalPreimageClaimAnchorZeroConf, + }, { Name: "multihop local preimage claim simple taproot", TestFunc: testLocalPreimageClaimSimpleTaproot, }, + { + Name: "multihop local preimage claim simple taproot zero conf", + TestFunc: testLocalPreimageClaimSimpleTaprootZeroConf, + }, { Name: "multihop local preimage claim leased", TestFunc: testLocalPreimageClaimLeased, }, + { + Name: "multihop local preimage claim leased zero conf", + TestFunc: testLocalPreimageClaimLeasedZeroConf, + }, { Name: "multihop htlc aggregation anchor", TestFunc: testHtlcAggregaitonAnchor, }, + { + Name: "multihop htlc aggregation anchor zero conf", + TestFunc: testHtlcAggregaitonAnchorZeroConf, + }, { Name: "multihop htlc aggregation simple taproot", TestFunc: testHtlcAggregaitonSimpleTaproot, }, + { + Name: "multihop htlc aggregation simple taproot zero conf", + TestFunc: testHtlcAggregaitonSimpleTaprootZeroConf, + }, { Name: "multihop htlc aggregation leased", TestFunc: testHtlcAggregaitonLeased, }, + { + Name: "multihop htlc aggregation leased zero conf", + TestFunc: testHtlcAggregaitonLeasedZeroConf, + }, } // 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} - // 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} - cfg := node.CfgAnchor - cfgCarol := append([]string{"--hodl.exit-settle"}, cfg...) - cfgs := [][]string{cfg, cfg, cfgCarol} + runLocalClaimOutgoingHTLC(ht, cfgs, openChannelParams) +} - runLocalClaimOutgoingHTLC(st, cfgs, openChannelParams) - }) - if !success { - return +// testLocalClaimOutgoingHTLCAnchorZeroConf tests `runLocalClaimOutgoingHTLC` +// with zero conf anchor channel. +func testLocalClaimOutgoingHTLCAnchorZeroConf(ht *lntest.HarnessTest) { + // 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, } - 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} + // 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) - }) + runLocalClaimOutgoingHTLC(ht, cfgs, openChannelParams) } // testLocalClaimOutgoingHTLCSimpleTaproot tests `runLocalClaimOutgoingHTLC` @@ -165,101 +240,87 @@ func testLocalClaimOutgoingHTLCAnchor(ht *lntest.HarnessTest) { 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, - } + // 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} + cfg := node.CfgSimpleTaproot + cfgCarol := append([]string{"--hodl.exit-settle"}, cfg...) + cfgs := [][]string{cfg, cfg, cfgCarol} - runLocalClaimOutgoingHTLC(st, cfgs, openChannelParams) - }) - if !success { - return - } + runLocalClaimOutgoingHTLC(ht, cfgs, openChannelParams) +} - ht.Run("zero conf", func(t *testing.T) { - st := ht.Subtest(t) +// testLocalClaimOutgoingHTLCSimpleTaprootZeroConf tests +// `runLocalClaimOutgoingHTLC` with zero-conf simple taproot channel. +func testLocalClaimOutgoingHTLCSimpleTaprootZeroConf(ht *lntest.HarnessTest) { + c := lnrpc.CommitmentType_SIMPLE_TAPROOT - // 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, - } + // 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} + // 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) - }) + runLocalClaimOutgoingHTLC(ht, 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, + } - // 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} - cfg := node.CfgLeased - cfgCarol := append([]string{"--hodl.exit-settle"}, cfg...) - cfgs := [][]string{cfg, cfg, cfgCarol} + runLocalClaimOutgoingHTLC(ht, cfgs, openChannelParams) +} - runLocalClaimOutgoingHTLC(st, cfgs, openChannelParams) - }) - if !success { - return +// testLocalClaimOutgoingHTLCLeasedZeroConf tests `runLocalClaimOutgoingHTLC` +// with zero-conf script enforced lease channel. +func testLocalClaimOutgoingHTLCLeasedZeroConf(ht *lntest.HarnessTest) { + // Create a three hop network: Alice -> Bob -> Carol, using zero-conf + // anchor channels. + // + // Prepare params. + openChannelParams := lntest.OpenChannelParams{ + Amt: chanAmt, + ZeroConf: true, + CommitmentType: leasedType, } - 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} - // 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(ht, cfgs, openChannelParams) } // runLocalClaimOutgoingHTLC tests that in a multi-hop scenario, if the @@ -452,43 +513,36 @@ func runLocalClaimOutgoingHTLC(ht *lntest.HarnessTest, // 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} - // 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} - cfg := node.CfgAnchor - cfgs := [][]string{cfg, cfg, cfg} + runMultiHopReceiverPreimageClaim(ht, cfgs, openChannelParams) +} - runMultiHopReceiverPreimageClaim(st, cfgs, openChannelParams) - }) - if !success { - return +// testMultiHopReceiverPreimageClaimAnchorZeroConf tests +// `runMultiHopReceiverPreimageClaim` with zero-conf anchor channels. +func testMultiHopReceiverPreimageClaimAnchorZeroConf(ht *lntest.HarnessTest) { + // 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, } - 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} + // Prepare Carol's node config to enable zero-conf and anchor. + cfg := node.CfgZeroConf + cfgs := [][]string{cfg, cfg, cfg} - runMultiHopReceiverPreimageClaim(st, cfgs, openChannelParams) - }) + runMultiHopReceiverPreimageClaim(ht, cfgs, openChannelParams) } // testMultiHopReceiverPreimageClaimSimpleTaproot tests @@ -496,97 +550,88 @@ func testMultiHopReceiverPreimageClaimAnchor(ht *lntest.HarnessTest) { 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, + } - // 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} - cfg := node.CfgSimpleTaproot - cfgs := [][]string{cfg, cfg, cfg} + runMultiHopReceiverPreimageClaim(ht, cfgs, openChannelParams) +} - runMultiHopReceiverPreimageClaim(st, cfgs, openChannelParams) - }) - if !success { - return - } +// testMultiHopReceiverPreimageClaimSimpleTaproot tests +// `runMultiHopReceiverPreimageClaim` with zero-conf simple taproot channels. +func testMultiHopReceiverPreimageClaimSimpleTaprootZeroConf( + ht *lntest.HarnessTest) { - ht.Run("zero conf", func(t *testing.T) { - st := ht.Subtest(t) + c := lnrpc.CommitmentType_SIMPLE_TAPROOT - // 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, - } + // 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} + // 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) - }) + runMultiHopReceiverPreimageClaim(ht, 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, + } - // 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} - cfg := node.CfgLeased - cfgs := [][]string{cfg, cfg, cfg} + runMultiHopReceiverPreimageClaim(ht, cfgs, openChannelParams) +} - runMultiHopReceiverPreimageClaim(st, cfgs, openChannelParams) - }) - if !success { - return +// testMultiHopReceiverPreimageClaimLeased tests +// `runMultiHopReceiverPreimageClaim` with zero-conf script enforce lease +// channels. +func testMultiHopReceiverPreimageClaimLeasedZeroConf(ht *lntest.HarnessTest) { + // Create a three hop network: Alice -> Bob -> Carol, using zero-conf + // anchor channels. + // + // Prepare params. + openChannelParams := lntest.OpenChannelParams{ + Amt: chanAmt, + ZeroConf: true, + CommitmentType: leasedType, } - 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} + // 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) - }) + runMultiHopReceiverPreimageClaim(ht, cfgs, openChannelParams) } // runMultiHopReceiverClaim tests that in the multi-hop setting, if the @@ -838,45 +883,38 @@ func runMultiHopReceiverPreimageClaim(ht *lntest.HarnessTest, // 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} - // 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} - cfg := node.CfgAnchor - cfgCarol := append([]string{"--hodl.exit-settle"}, cfg...) - cfgs := [][]string{cfg, cfg, cfgCarol} + runLocalForceCloseBeforeHtlcTimeout(ht, cfgs, params) +} - runLocalForceCloseBeforeHtlcTimeout(st, cfgs, params) - }) - if !success { - return +// testLocalForceCloseBeforeTimeoutAnchorZeroConf tests +// `runLocalForceCloseBeforeHtlcTimeout` with zero-conf anchor channel. +func testLocalForceCloseBeforeTimeoutAnchorZeroConf(ht *lntest.HarnessTest) { + // 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, } - 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} + // 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) - }) + runLocalForceCloseBeforeHtlcTimeout(ht, cfgs, params) } // testLocalForceCloseBeforeTimeoutSimpleTaproot tests @@ -884,101 +922,90 @@ func testLocalForceCloseBeforeTimeoutAnchor(ht *lntest.HarnessTest) { 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, + } - // 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} - cfg := node.CfgSimpleTaproot - cfgCarol := append([]string{"--hodl.exit-settle"}, cfg...) - cfgs := [][]string{cfg, cfg, cfgCarol} + runLocalForceCloseBeforeHtlcTimeout(ht, cfgs, params) +} - runLocalForceCloseBeforeHtlcTimeout(st, cfgs, params) - }) - if !success { - return - } +// testLocalForceCloseBeforeTimeoutSimpleTaproot tests +// `runLocalForceCloseBeforeHtlcTimeout` with zero-conf simple taproot channel. +func testLocalForceCloseBeforeTimeoutSimpleTaprootZeroConf( + ht *lntest.HarnessTest) { - ht.Run("zero conf", func(t *testing.T) { - st := ht.Subtest(t) + c := lnrpc.CommitmentType_SIMPLE_TAPROOT - // 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, - } + // 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} + // 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) - }) + runLocalForceCloseBeforeHtlcTimeout(ht, 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, + } - // 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} - cfg := node.CfgLeased - cfgCarol := append([]string{"--hodl.exit-settle"}, cfg...) - cfgs := [][]string{cfg, cfg, cfgCarol} + runLocalForceCloseBeforeHtlcTimeout(ht, cfgs, params) +} - runLocalForceCloseBeforeHtlcTimeout(st, cfgs, params) - }) - if !success { - return +// testLocalForceCloseBeforeTimeoutLeased tests +// `runLocalForceCloseBeforeHtlcTimeout` with zero-conf script enforced lease +// channel. +func testLocalForceCloseBeforeTimeoutLeasedZeroConf(ht *lntest.HarnessTest) { + // Create a three hop network: Alice -> Bob -> Carol, using zero-conf + // anchor channels. + // + // Prepare params. + params := lntest.OpenChannelParams{ + Amt: chanAmt, + ZeroConf: true, + CommitmentType: leasedType, } - 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} + // 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(ht, cfgs, params) } // runLocalForceCloseBeforeHtlcTimeout tests that in a multi-hop HTLC scenario, @@ -1170,45 +1197,65 @@ func runLocalForceCloseBeforeHtlcTimeout(ht *lntest.HarnessTest, // 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} - // 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} - cfg := node.CfgAnchor - cfgCarol := append([]string{"--hodl.exit-settle"}, cfg...) - cfgs := [][]string{cfg, cfg, cfgCarol} + runRemoteForceCloseBeforeHtlcTimeout(ht, cfgs, params) +} - runRemoteForceCloseBeforeHtlcTimeout(st, cfgs, params) - }) - if !success { - return +// testRemoteForceCloseBeforeTimeoutAnchor tests +// `runRemoteForceCloseBeforeHtlcTimeout` with zero-conf anchor channel. +func testRemoteForceCloseBeforeTimeoutAnchorZeroConf(ht *lntest.HarnessTest) { + // 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, } - ht.Run("zero conf", func(t *testing.T) { - st := ht.Subtest(t) + // 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} - // 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, - } + runRemoteForceCloseBeforeHtlcTimeout(ht, cfgs, params) +} - // 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} +// testRemoteForceCloseBeforeTimeoutSimpleTaproot tests +// `runLocalForceCloseBeforeHtlcTimeout` with zero-conf simple taproot channel. +func testRemoteForceCloseBeforeTimeoutSimpleTaprootZeroConf( + ht *lntest.HarnessTest) { + + c := lnrpc.CommitmentType_SIMPLE_TAPROOT - runRemoteForceCloseBeforeHtlcTimeout(st, cfgs, params) - }) + // 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(ht, cfgs, params) } // testRemoteForceCloseBeforeTimeoutSimpleTaproot tests @@ -1216,101 +1263,64 @@ func testRemoteForceCloseBeforeTimeoutAnchor(ht *lntest.HarnessTest) { 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, + } - // 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} - cfg := node.CfgSimpleTaproot - cfgCarol := append([]string{"--hodl.exit-settle"}, cfg...) - cfgs := [][]string{cfg, cfg, cfgCarol} + runRemoteForceCloseBeforeHtlcTimeout(ht, cfgs, params) +} - runRemoteForceCloseBeforeHtlcTimeout(st, cfgs, params) - }) - if !success { - return +// testRemoteForceCloseBeforeTimeoutLeasedZeroConf tests +// `runRemoteForceCloseBeforeHtlcTimeout` with zero-conf script enforced lease +// channel. +func testRemoteForceCloseBeforeTimeoutLeasedZeroConf(ht *lntest.HarnessTest) { + // Create a three hop network: Alice -> Bob -> Carol, using zero-conf + // anchor channels. + // + // Prepare params. + params := lntest.OpenChannelParams{ + Amt: chanAmt, + ZeroConf: true, + CommitmentType: leasedType, } - ht.Run("zero conf", func(t *testing.T) { - st := ht.Subtest(t) + // 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} - // 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) - }) + runRemoteForceCloseBeforeHtlcTimeout(ht, 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 + // Create a three hop network: Alice -> Bob -> Carol, using leased + // channels. + // + // Prepare params. + params := lntest.OpenChannelParams{ + Amt: chanAmt, + CommitmentType: leasedType, } - 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} + cfg := node.CfgLeased + cfgCarol := append([]string{"--hodl.exit-settle"}, cfg...) + cfgs := [][]string{cfg, cfg, cfgCarol} - runRemoteForceCloseBeforeHtlcTimeout(st, cfgs, params) - }) + runRemoteForceCloseBeforeHtlcTimeout(ht, cfgs, params) } // runRemoteForceCloseBeforeHtlcTimeout tests that if we extend a multi-hop @@ -1481,46 +1491,63 @@ func runRemoteForceCloseBeforeHtlcTimeout(ht *lntest.HarnessTest, ht.AssertInvoiceState(stream, lnrpc.Invoice_CANCELED) } +// testLocalClaimIncomingHTLCAnchorZeroConf tests `runLocalClaimIncomingHTLC` +// with zero-conf anchor channel. +func testLocalClaimIncomingHTLCAnchorZeroConf(ht *lntest.HarnessTest) { + // 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(ht, cfgs, params) +} + // 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} + // 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} + cfg := node.CfgAnchor + cfgs := [][]string{cfg, cfg, cfg} - runLocalClaimIncomingHTLC(st, cfgs, params) - }) - if !success { - return - } + runLocalClaimIncomingHTLC(ht, cfgs, params) +} - ht.Run("zero conf", func(t *testing.T) { - st := ht.Subtest(t) +// testLocalClaimIncomingHTLCSimpleTaprootZeroConf tests +// `runLocalClaimIncomingHTLC` with zero-conf simple taproot channel. +func testLocalClaimIncomingHTLCSimpleTaprootZeroConf(ht *lntest.HarnessTest) { + c := lnrpc.CommitmentType_SIMPLE_TAPROOT - // 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, - } + // 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 anchor. - cfg := node.CfgZeroConf - cfgs := [][]string{cfg, cfg, cfg} + // 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(ht, cfgs, params) } // testLocalClaimIncomingHTLCSimpleTaproot tests `runLocalClaimIncomingHTLC` @@ -1528,50 +1555,20 @@ func testLocalClaimIncomingHTLCAnchor(ht *lntest.HarnessTest) { 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 + // Create a three hop network: Alice -> Bob -> Carol, using simple + // taproot channels. + // + // Prepare params. + params := lntest.OpenChannelParams{ + Amt: chanAmt, + CommitmentType: c, + Private: true, } - ht.Run("zero conf", func(t *testing.T) { - st := ht.Subtest(t) + cfg := node.CfgSimpleTaproot + cfgs := [][]string{cfg, cfg, cfg} - // 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(ht, cfgs, params) } // runLocalClaimIncomingHTLC tests that in a multi-hop HTLC scenario, if we @@ -1845,51 +1842,44 @@ func runLocalClaimIncomingHTLC(ht *lntest.HarnessTest, 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) +// testLocalClaimIncomingHTLCLeasedZeroConf tests +// `runLocalClaimIncomingHTLCLeased` with zero-conf script enforced lease +// channel. +func testLocalClaimIncomingHTLCLeasedZeroConf(ht *lntest.HarnessTest) { + // Create a three hop network: Alice -> Bob -> Carol, using zero-conf + // anchor channels. + // + // Prepare params. + params := lntest.OpenChannelParams{ + Amt: chanAmt, + ZeroConf: true, + CommitmentType: leasedType, + } - // Create a three hop network: Alice -> Bob -> Carol, using - // leased channels. - // - // Prepare params. - params := lntest.OpenChannelParams{ - Amt: chanAmt, - 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} - cfg := node.CfgLeased - cfgs := [][]string{cfg, cfg, cfg} + runLocalClaimIncomingHTLCLeased(ht, cfgs, params) +} - runLocalClaimIncomingHTLCLeased(st, cfgs, params) - }) - if !success { - return +// testLocalClaimIncomingHTLCLeased tests `runLocalClaimIncomingHTLCLeased` +// with script enforced lease channel. +func testLocalClaimIncomingHTLCLeased(ht *lntest.HarnessTest) { + // Create a three hop network: Alice -> Bob -> Carol, using leased + // channels. + // + // Prepare params. + params := lntest.OpenChannelParams{ + Amt: chanAmt, + CommitmentType: leasedType, } - ht.Run("zero conf", func(t *testing.T) { - st := ht.Subtest(t) + cfg := node.CfgLeased + cfgs := [][]string{cfg, cfg, cfg} - // 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(ht, cfgs, params) } // runLocalClaimIncomingHTLCLeased tests that in a multi-hop HTLC scenario, if @@ -2148,46 +2138,63 @@ func runLocalClaimIncomingHTLCLeased(ht *lntest.HarnessTest, ht.AssertPaymentStatus(alice, preimage, lnrpc.Payment_SUCCEEDED) } +// testLocalPreimageClaimAnchorZeroConf tests `runLocalPreimageClaim` with +// zero-conf anchor channel. +func testLocalPreimageClaimAnchorZeroConf(ht *lntest.HarnessTest) { + // 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(ht, cfgs, params) +} + // 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} + // 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} + cfg := node.CfgAnchor + cfgs := [][]string{cfg, cfg, cfg} - runLocalPreimageClaim(st, cfgs, params) - }) - if !success { - return - } + runLocalPreimageClaim(ht, cfgs, params) +} - ht.Run("zero conf", func(t *testing.T) { - st := ht.Subtest(t) +// testLocalPreimageClaimSimpleTaprootZeroConf tests +// `runLocalClaimIncomingHTLC` with zero-conf simple taproot channel. +func testLocalPreimageClaimSimpleTaprootZeroConf(ht *lntest.HarnessTest) { + c := lnrpc.CommitmentType_SIMPLE_TAPROOT - // 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, - } + // 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 anchor. - cfg := node.CfgZeroConf - cfgs := [][]string{cfg, cfg, cfg} + // 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(ht, cfgs, params) } // testLocalPreimageClaimSimpleTaproot tests `runLocalClaimIncomingHTLC` with @@ -2195,50 +2202,20 @@ func testLocalPreimageClaimAnchor(ht *lntest.HarnessTest) { 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 + // Create a three hop network: Alice -> Bob -> Carol, using simple + // taproot channels. + // + // Prepare params. + params := lntest.OpenChannelParams{ + Amt: chanAmt, + CommitmentType: c, + Private: true, } - 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} + cfg := node.CfgSimpleTaproot + cfgs := [][]string{cfg, cfg, cfg} - runLocalPreimageClaim(st, cfgs, params) - }) + runLocalPreimageClaim(ht, cfgs, params) } // runLocalPreimageClaim tests that in the multi-hop HTLC scenario, if the @@ -2500,51 +2477,43 @@ func runLocalPreimageClaim(ht *lntest.HarnessTest, 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) +// testLocalPreimageClaimLeasedZeroConf tests `runLocalPreimageClaim` with +// zero-conf script enforced lease channel. +func testLocalPreimageClaimLeasedZeroConf(ht *lntest.HarnessTest) { + // Create a three hop network: Alice -> Bob -> Carol, using zero-conf + // anchor channels. + // + // Prepare params. + params := lntest.OpenChannelParams{ + Amt: chanAmt, + ZeroConf: true, + CommitmentType: leasedType, + } - // Create a three hop network: Alice -> Bob -> Carol, using - // leased channels. - // - // Prepare params. - params := lntest.OpenChannelParams{ - Amt: chanAmt, - 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} - cfg := node.CfgLeased - cfgs := [][]string{cfg, cfg, cfg} + runLocalPreimageClaimLeased(ht, cfgs, params) +} - runLocalPreimageClaimLeased(st, cfgs, params) - }) - if !success { - return +// testLocalPreimageClaimLeased tests `runLocalPreimageClaim` with script +// enforced lease channel. +func testLocalPreimageClaimLeased(ht *lntest.HarnessTest) { + // Create a three hop network: Alice -> Bob -> Carol, using leased + // channels. + // + // Prepare params. + params := lntest.OpenChannelParams{ + Amt: chanAmt, + CommitmentType: leasedType, } - ht.Run("zero conf", func(t *testing.T) { - st := ht.Subtest(t) + cfg := node.CfgLeased + cfgs := [][]string{cfg, cfg, cfg} - // 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(ht, cfgs, params) } // runLocalPreimageClaimLeased tests that in the multi-hop HTLC scenario, if @@ -2782,45 +2751,62 @@ func runLocalPreimageClaimLeased(ht *lntest.HarnessTest, ht.AssertNumPendingForceClose(bob, 0) } +// testHtlcAggregaitonAnchor tests `runHtlcAggregation` with zero-conf anchor +// channel. +func testHtlcAggregaitonAnchorZeroConf(ht *lntest.HarnessTest) { + // 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(ht, cfgs, params) +} + // 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} + // 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} + cfg := node.CfgAnchor + cfgs := [][]string{cfg, cfg, cfg} - runHtlcAggregation(st, cfgs, params) - }) - if !success { - return - } + runHtlcAggregation(ht, cfgs, params) +} - ht.Run("zero conf", func(t *testing.T) { - st := ht.Subtest(t) +// testHtlcAggregaitonSimpleTaprootZeroConf tests `runHtlcAggregation` with +// zero-conf simple taproot channel. +func testHtlcAggregaitonSimpleTaprootZeroConf(ht *lntest.HarnessTest) { + c := lnrpc.CommitmentType_SIMPLE_TAPROOT - // 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, - } + // 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 anchor. - cfg := node.CfgZeroConf - cfgs := [][]string{cfg, cfg, cfg} + // 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) - }) + runHtlcAggregation(ht, cfgs, params) } // testHtlcAggregaitonSimpleTaproot tests `runHtlcAggregation` with simple @@ -2828,97 +2814,59 @@ func testHtlcAggregaitonAnchor(ht *lntest.HarnessTest) { 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, + } - // 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} - cfg := node.CfgSimpleTaproot - cfgs := [][]string{cfg, cfg, cfg} + runHtlcAggregation(ht, cfgs, params) +} - runHtlcAggregation(st, cfgs, params) - }) - if !success { - return +// testHtlcAggregaitonLeasedZeroConf tests `runHtlcAggregation` with zero-conf +// script enforced lease channel. +func testHtlcAggregaitonLeasedZeroConf(ht *lntest.HarnessTest) { + // Create a three hop network: Alice -> Bob -> Carol, using zero-conf + // anchor channels. + // + // Prepare params. + params := lntest.OpenChannelParams{ + Amt: chanAmt, + ZeroConf: true, + CommitmentType: leasedType, } - 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} + // 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(ht, 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 + // Create a three hop network: Alice -> Bob -> Carol, using leased + // channels. + // + // Prepare params. + params := lntest.OpenChannelParams{ + Amt: chanAmt, + CommitmentType: leasedType, } - 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} + cfg := node.CfgLeased + cfgs := [][]string{cfg, cfg, cfg} - runHtlcAggregation(st, cfgs, params) - }) + runHtlcAggregation(ht, cfgs, params) } // runHtlcAggregation tests that in a multi-hop HTLC scenario, if we force From 006f656c2d3e394381ab73e119489e4bc010ebc6 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 9 Nov 2024 00:37:50 +0800 Subject: [PATCH 128/153] itest: break down scid alias channel update tests --- itest/list_on_test.go | 5 +- itest/lnd_zero_conf_test.go | 116 +++++++++++++++++++----------------- 2 files changed, 62 insertions(+), 59 deletions(-) diff --git a/itest/list_on_test.go b/itest/list_on_test.go index d51afc05b9..9fec3eb2b4 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -448,10 +448,6 @@ var allTestCases = []*lntest.TestCase{ Name: "option scid alias", TestFunc: testOptionScidAlias, }, - { - Name: "scid alias channel update", - TestFunc: testUpdateChannelPolicyScidAlias, - }, { Name: "scid alias upgrade", TestFunc: testOptionScidUpgrade, @@ -678,4 +674,5 @@ func init() { allTestCases = append(allTestCases, remoteSignerTestCases...) allTestCases = append(allTestCases, channelRestoreTestCases...) allTestCases = append(allTestCases, fundUtxoSelectionTestCases...) + allTestCases = append(allTestCases, zeroConfPolicyTestCases...) } diff --git a/itest/lnd_zero_conf_test.go b/itest/lnd_zero_conf_test.go index c7f4c59a78..5b87846be5 100644 --- a/itest/lnd_zero_conf_test.go +++ b/itest/lnd_zero_conf_test.go @@ -19,6 +19,67 @@ import ( "github.com/stretchr/testify/require" ) +// zeroConfPolicyTestCases checks that option-scid-alias, zero-conf +// channel-types, and option-scid-alias feature-bit-only channels have the +// expected graph and that payments work when updating the channel policy. +var zeroConfPolicyTestCases = []*lntest.TestCase{ + { + Name: "channel policy update private", + TestFunc: func(ht *lntest.HarnessTest) { + // zeroConf: false + // scidAlias: false + // private: true + testPrivateUpdateAlias( + ht, false, false, true, + ) + }, + }, + { + Name: "channel policy update private scid alias", + TestFunc: func(ht *lntest.HarnessTest) { + // zeroConf: false + // scidAlias: true + // private: true + testPrivateUpdateAlias( + ht, false, true, true, + ) + }, + }, + { + Name: "channel policy update private zero conf", + TestFunc: func(ht *lntest.HarnessTest) { + // zeroConf: true + // scidAlias: false + // private: true + testPrivateUpdateAlias( + ht, true, false, true, + ) + }, + }, + { + Name: "channel policy update public zero conf", + TestFunc: func(ht *lntest.HarnessTest) { + // zeroConf: true + // scidAlias: false + // private: false + testPrivateUpdateAlias( + ht, true, false, false, + ) + }, + }, + { + Name: "channel policy update public", + TestFunc: func(ht *lntest.HarnessTest) { + // zeroConf: false + // scidAlias: false + // private: false + testPrivateUpdateAlias( + ht, false, false, false, + ) + }, + }, +} + // testZeroConfChannelOpen tests that opening a zero-conf channel works and // sending payments also works. func testZeroConfChannelOpen(ht *lntest.HarnessTest) { @@ -395,61 +456,6 @@ func waitForZeroConfGraphChange(hn *node.HarnessNode, }, defaultTimeout) } -// testUpdateChannelPolicyScidAlias checks that option-scid-alias, zero-conf -// channel-types, and option-scid-alias feature-bit-only channels have the -// expected graph and that payments work when updating the channel policy. -func testUpdateChannelPolicyScidAlias(ht *lntest.HarnessTest) { - tests := []struct { - name string - - // The option-scid-alias channel type. - scidAliasType bool - - // The zero-conf channel type. - zeroConf bool - - private bool - }{ - { - name: "private scid-alias chantype update", - scidAliasType: true, - private: true, - }, - { - name: "private zero-conf update", - zeroConf: true, - private: true, - }, - { - name: "public zero-conf update", - zeroConf: true, - }, - { - name: "public no-chan-type update", - }, - { - name: "private no-chan-type update", - private: true, - }, - } - - for _, test := range tests { - test := test - - success := ht.Run(test.name, func(t *testing.T) { - st := ht.Subtest(t) - - testPrivateUpdateAlias( - st, test.zeroConf, test.scidAliasType, - test.private, - ) - }) - if !success { - return - } - } -} - func testPrivateUpdateAlias(ht *lntest.HarnessTest, zeroConf, scidAliasType, private bool) { From 676b925e5827e560f4f2c28594131ae56824a803 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 9 Nov 2024 00:55:54 +0800 Subject: [PATCH 129/153] itest: break down open channel fee policy --- itest/list_on_test.go | 5 +- itest/lnd_open_channel_test.go | 407 +++++++++++++++++++++------------ 2 files changed, 268 insertions(+), 144 deletions(-) diff --git a/itest/list_on_test.go b/itest/list_on_test.go index 9fec3eb2b4..b4642f3b68 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -512,10 +512,6 @@ var allTestCases = []*lntest.TestCase{ Name: "trackpayments compatible", TestFunc: testTrackPaymentsCompatible, }, - { - Name: "open channel fee policy", - TestFunc: testOpenChannelUpdateFeePolicy, - }, { Name: "custom message", TestFunc: testCustomMessage, @@ -675,4 +671,5 @@ func init() { allTestCases = append(allTestCases, channelRestoreTestCases...) allTestCases = append(allTestCases, fundUtxoSelectionTestCases...) allTestCases = append(allTestCases, zeroConfPolicyTestCases...) + allTestCases = append(allTestCases, channelFeePolicyTestCases...) } diff --git a/itest/lnd_open_channel_test.go b/itest/lnd_open_channel_test.go index 3142e09eb2..9d51fc5635 100644 --- a/itest/lnd_open_channel_test.go +++ b/itest/lnd_open_channel_test.go @@ -3,7 +3,6 @@ package itest import ( "fmt" "strings" - "testing" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" @@ -18,6 +17,31 @@ import ( "github.com/stretchr/testify/require" ) +// channelFeePolicyTestCases defines a set of tests to check the update channel +// policy fee behavior. +var channelFeePolicyTestCases = []*lntest.TestCase{ + { + Name: "channel fee policy default", + TestFunc: testChannelFeePolicyDefault, + }, + { + Name: "channel fee policy base fee", + TestFunc: testChannelFeePolicyBaseFee, + }, + { + Name: "channel fee policy fee rate", + TestFunc: testChannelFeePolicyFeeRate, + }, + { + Name: "channel fee policy base fee and fee rate", + TestFunc: testChannelFeePolicyBaseFeeAndFeeRate, + }, + { + Name: "channel fee policy low base fee and fee rate", + TestFunc: testChannelFeePolicyLowBaseFeeAndFeeRate, + }, +} + // testOpenChannelAfterReorg tests that in the case where we have an open // channel where the funding tx gets reorged out, the channel will no // longer be present in the node's routing table. @@ -123,32 +147,103 @@ func testOpenChannelAfterReorg(ht *lntest.HarnessTest) { ht.AssertTxInBlock(block, *fundingTxID) } -// testOpenChannelFeePolicy checks if different channel fee scenarios are -// correctly handled when the optional channel fee parameters baseFee and -// feeRate are provided. If the OpenChannelRequest is not provided with a value -// for baseFee/feeRate the expectation is that the default baseFee/feeRate is -// applied. -// -// 1. No params provided to OpenChannelRequest: -// ChannelUpdate --> defaultBaseFee, defaultFeeRate -// 2. Only baseFee provided to OpenChannelRequest: -// ChannelUpdate --> provided baseFee, defaultFeeRate -// 3. Only feeRate provided to OpenChannelRequest: -// ChannelUpdate --> defaultBaseFee, provided FeeRate -// 4. baseFee and feeRate provided to OpenChannelRequest: -// ChannelUpdate --> provided baseFee, provided feeRate -// 5. Both baseFee and feeRate are set to a value lower than the default: -// ChannelUpdate --> provided baseFee, provided feeRate -func testOpenChannelUpdateFeePolicy(ht *lntest.HarnessTest) { +// testChannelFeePolicyDefault check when no params provided to +// OpenChannelRequest: ChannelUpdate --> defaultBaseFee, defaultFeeRate. +func testChannelFeePolicyDefault(ht *lntest.HarnessTest) { + const ( + defaultBaseFee = 1000 + defaultFeeRate = 1 + defaultTimeLockDelta = chainreg.DefaultBitcoinTimeLockDelta + defaultMinHtlc = 1000 + ) + + defaultMaxHtlc := lntest.CalculateMaxHtlc(funding.MaxBtcFundingAmount) + + chanAmt := funding.MaxBtcFundingAmount + pushAmt := chanAmt / 2 + + feeScenario := lntest.OpenChannelParams{ + Amt: chanAmt, + PushAmt: pushAmt, + UseBaseFee: false, + UseFeeRate: false, + } + + expectedPolicy := lnrpc.RoutingPolicy{ + FeeBaseMsat: defaultBaseFee, + FeeRateMilliMsat: defaultFeeRate, + TimeLockDelta: defaultTimeLockDelta, + MinHtlc: defaultMinHtlc, + MaxHtlcMsat: defaultMaxHtlc, + } + + bobExpectedPolicy := lnrpc.RoutingPolicy{ + FeeBaseMsat: defaultBaseFee, + FeeRateMilliMsat: defaultFeeRate, + TimeLockDelta: defaultTimeLockDelta, + MinHtlc: defaultMinHtlc, + MaxHtlcMsat: defaultMaxHtlc, + } + + runChannelFeePolicyTest( + ht, feeScenario, &expectedPolicy, &bobExpectedPolicy, + ) +} + +// testChannelFeePolicyBaseFee checks only baseFee provided to +// OpenChannelRequest: ChannelUpdate --> provided baseFee, defaultFeeRate. +func testChannelFeePolicyBaseFee(ht *lntest.HarnessTest) { const ( defaultBaseFee = 1000 defaultFeeRate = 1 defaultTimeLockDelta = chainreg.DefaultBitcoinTimeLockDelta defaultMinHtlc = 1000 optionalBaseFee = 1337 + ) + + defaultMaxHtlc := lntest.CalculateMaxHtlc(funding.MaxBtcFundingAmount) + + chanAmt := funding.MaxBtcFundingAmount + pushAmt := chanAmt / 2 + + feeScenario := lntest.OpenChannelParams{ + Amt: chanAmt, + PushAmt: pushAmt, + BaseFee: optionalBaseFee, + UseBaseFee: true, + UseFeeRate: false, + } + + expectedPolicy := lnrpc.RoutingPolicy{ + FeeBaseMsat: optionalBaseFee, + FeeRateMilliMsat: defaultFeeRate, + TimeLockDelta: defaultTimeLockDelta, + MinHtlc: defaultMinHtlc, + MaxHtlcMsat: defaultMaxHtlc, + } + + bobExpectedPolicy := lnrpc.RoutingPolicy{ + FeeBaseMsat: defaultBaseFee, + FeeRateMilliMsat: defaultFeeRate, + TimeLockDelta: defaultTimeLockDelta, + MinHtlc: defaultMinHtlc, + MaxHtlcMsat: defaultMaxHtlc, + } + + runChannelFeePolicyTest( + ht, feeScenario, &expectedPolicy, &bobExpectedPolicy, + ) +} + +// testChannelFeePolicyFeeRate checks if only feeRate provided to +// OpenChannelRequest: ChannelUpdate --> defaultBaseFee, provided FeeRate. +func testChannelFeePolicyFeeRate(ht *lntest.HarnessTest) { + const ( + defaultBaseFee = 1000 + defaultFeeRate = 1 + defaultTimeLockDelta = chainreg.DefaultBitcoinTimeLockDelta + defaultMinHtlc = 1000 optionalFeeRate = 1337 - lowBaseFee = 0 - lowFeeRate = 900 ) defaultMaxHtlc := lntest.CalculateMaxHtlc(funding.MaxBtcFundingAmount) @@ -156,81 +251,20 @@ func testOpenChannelUpdateFeePolicy(ht *lntest.HarnessTest) { chanAmt := funding.MaxBtcFundingAmount pushAmt := chanAmt / 2 - feeScenarios := []lntest.OpenChannelParams{ - { - Amt: chanAmt, - PushAmt: pushAmt, - UseBaseFee: false, - UseFeeRate: false, - }, - { - Amt: chanAmt, - PushAmt: pushAmt, - BaseFee: optionalBaseFee, - UseBaseFee: true, - UseFeeRate: false, - }, - { - Amt: chanAmt, - PushAmt: pushAmt, - FeeRate: optionalFeeRate, - UseBaseFee: false, - UseFeeRate: true, - }, - { - Amt: chanAmt, - PushAmt: pushAmt, - BaseFee: optionalBaseFee, - FeeRate: optionalFeeRate, - UseBaseFee: true, - UseFeeRate: true, - }, - { - Amt: chanAmt, - PushAmt: pushAmt, - BaseFee: lowBaseFee, - FeeRate: lowFeeRate, - UseBaseFee: true, - UseFeeRate: true, - }, + feeScenario := lntest.OpenChannelParams{ + Amt: chanAmt, + PushAmt: pushAmt, + FeeRate: optionalFeeRate, + UseBaseFee: false, + UseFeeRate: true, } - expectedPolicies := []lnrpc.RoutingPolicy{ - { - FeeBaseMsat: defaultBaseFee, - FeeRateMilliMsat: defaultFeeRate, - TimeLockDelta: defaultTimeLockDelta, - MinHtlc: defaultMinHtlc, - MaxHtlcMsat: defaultMaxHtlc, - }, - { - FeeBaseMsat: optionalBaseFee, - FeeRateMilliMsat: defaultFeeRate, - TimeLockDelta: defaultTimeLockDelta, - MinHtlc: defaultMinHtlc, - MaxHtlcMsat: defaultMaxHtlc, - }, - { - FeeBaseMsat: defaultBaseFee, - FeeRateMilliMsat: optionalFeeRate, - TimeLockDelta: defaultTimeLockDelta, - MinHtlc: defaultMinHtlc, - MaxHtlcMsat: defaultMaxHtlc, - }, - { - FeeBaseMsat: optionalBaseFee, - FeeRateMilliMsat: optionalFeeRate, - TimeLockDelta: defaultTimeLockDelta, - MinHtlc: defaultMinHtlc, - MaxHtlcMsat: defaultMaxHtlc, - }, - { - FeeBaseMsat: lowBaseFee, - FeeRateMilliMsat: lowFeeRate, - TimeLockDelta: defaultTimeLockDelta, - MinHtlc: defaultMinHtlc, - MaxHtlcMsat: defaultMaxHtlc, - }, + expectedPolicy := lnrpc.RoutingPolicy{ + FeeBaseMsat: defaultBaseFee, + FeeRateMilliMsat: optionalFeeRate, + TimeLockDelta: defaultTimeLockDelta, + MinHtlc: defaultMinHtlc, + MaxHtlcMsat: defaultMaxHtlc, } bobExpectedPolicy := lnrpc.RoutingPolicy{ @@ -241,65 +275,158 @@ func testOpenChannelUpdateFeePolicy(ht *lntest.HarnessTest) { MaxHtlcMsat: defaultMaxHtlc, } - runTestCase := func(ht *lntest.HarnessTest, - chanParams lntest.OpenChannelParams, - alicePolicy, bobPolicy *lnrpc.RoutingPolicy) { + runChannelFeePolicyTest( + ht, feeScenario, &expectedPolicy, &bobExpectedPolicy, + ) +} - // In this basic test, we'll need a third node, Carol, so we - // can forward a payment through the channel we'll open with - // the different fee policies. - alice := ht.NewNodeWithCoins("Alice", nil) - bob := ht.NewNode("Bob", nil) - carol := ht.NewNodeWithCoins("Carol", nil) +// testChannelFeePolicyBaseFeeAndFeeRate checks if baseFee and feeRate provided +// to OpenChannelRequest: ChannelUpdate --> provided baseFee, provided feeRate. +func testChannelFeePolicyBaseFeeAndFeeRate(ht *lntest.HarnessTest) { + const ( + defaultBaseFee = 1000 + defaultFeeRate = 1 + defaultTimeLockDelta = chainreg.DefaultBitcoinTimeLockDelta + defaultMinHtlc = 1000 + optionalBaseFee = 1337 + optionalFeeRate = 1337 + ) - ht.EnsureConnected(alice, bob) - ht.EnsureConnected(alice, carol) + defaultMaxHtlc := lntest.CalculateMaxHtlc(funding.MaxBtcFundingAmount) - nodes := []*node.HarnessNode{alice, bob, carol} + chanAmt := funding.MaxBtcFundingAmount + pushAmt := chanAmt / 2 - // Create a channel Alice->Bob. - chanPoint := ht.OpenChannel(alice, bob, chanParams) + feeScenario := lntest.OpenChannelParams{ + Amt: chanAmt, + PushAmt: pushAmt, + BaseFee: optionalBaseFee, + FeeRate: optionalFeeRate, + UseBaseFee: true, + UseFeeRate: true, + } - // Create a channel Carol->Alice. - ht.OpenChannel( - carol, alice, lntest.OpenChannelParams{ - Amt: 500000, - }, - ) + expectedPolicy := lnrpc.RoutingPolicy{ + FeeBaseMsat: optionalBaseFee, + FeeRateMilliMsat: optionalFeeRate, + TimeLockDelta: defaultTimeLockDelta, + MinHtlc: defaultMinHtlc, + MaxHtlcMsat: defaultMaxHtlc, + } - // Alice and Bob should see each other's ChannelUpdates, - // advertising the preferred routing policies. - assertNodesPolicyUpdate( - ht, nodes, alice, alicePolicy, chanPoint, - ) - assertNodesPolicyUpdate(ht, nodes, bob, bobPolicy, chanPoint) + bobExpectedPolicy := lnrpc.RoutingPolicy{ + FeeBaseMsat: defaultBaseFee, + FeeRateMilliMsat: defaultFeeRate, + TimeLockDelta: defaultTimeLockDelta, + MinHtlc: defaultMinHtlc, + MaxHtlcMsat: defaultMaxHtlc, + } - // They should now know about the default policies. - for _, n := range nodes { - ht.AssertChannelPolicy( - n, alice.PubKeyStr, alicePolicy, chanPoint, - ) - ht.AssertChannelPolicy( - n, bob.PubKeyStr, bobPolicy, chanPoint, - ) - } + runChannelFeePolicyTest( + ht, feeScenario, &expectedPolicy, &bobExpectedPolicy, + ) +} + +// testChannelFeePolicyLowBaseFeeAndFeeRate checks if both baseFee and feeRate +// are set to a value lower than the default: ChannelUpdate --> provided +// baseFee, provided feeRate. +func testChannelFeePolicyLowBaseFeeAndFeeRate(ht *lntest.HarnessTest) { + const ( + defaultBaseFee = 1000 + defaultFeeRate = 1 + defaultTimeLockDelta = chainreg.DefaultBitcoinTimeLockDelta + defaultMinHtlc = 1000 + lowBaseFee = 0 + lowFeeRate = 900 + ) - // We should be able to forward a payment from Carol to Bob - // through the new channel we opened. - payReqs, _, _ := ht.CreatePayReqs(bob, paymentAmt, 1) - ht.CompletePaymentRequests(carol, payReqs) + defaultMaxHtlc := lntest.CalculateMaxHtlc(funding.MaxBtcFundingAmount) + + chanAmt := funding.MaxBtcFundingAmount + pushAmt := chanAmt / 2 + + feeScenario := lntest.OpenChannelParams{ + Amt: chanAmt, + PushAmt: pushAmt, + BaseFee: lowBaseFee, + FeeRate: lowFeeRate, + UseBaseFee: true, + UseFeeRate: true, } - for i, feeScenario := range feeScenarios { - ht.Run(fmt.Sprintf("%d", i), func(t *testing.T) { - st := ht.Subtest(t) + expectedPolicy := lnrpc.RoutingPolicy{ + FeeBaseMsat: lowBaseFee, + FeeRateMilliMsat: lowFeeRate, + TimeLockDelta: defaultTimeLockDelta, + MinHtlc: defaultMinHtlc, + MaxHtlcMsat: defaultMaxHtlc, + } - runTestCase( - st, feeScenario, - &expectedPolicies[i], &bobExpectedPolicy, - ) - }) + bobExpectedPolicy := lnrpc.RoutingPolicy{ + FeeBaseMsat: defaultBaseFee, + FeeRateMilliMsat: defaultFeeRate, + TimeLockDelta: defaultTimeLockDelta, + MinHtlc: defaultMinHtlc, + MaxHtlcMsat: defaultMaxHtlc, } + + runChannelFeePolicyTest( + ht, feeScenario, &expectedPolicy, &bobExpectedPolicy, + ) +} + +// runChannelFeePolicyTest checks if different channel fee scenarios are +// correctly handled when the optional channel fee parameters baseFee and +// feeRate are provided. If the OpenChannelRequest is not provided with a value +// for baseFee/feeRate the expectation is that the default baseFee/feeRate is +// applied. +func runChannelFeePolicyTest(ht *lntest.HarnessTest, + chanParams lntest.OpenChannelParams, + alicePolicy, bobPolicy *lnrpc.RoutingPolicy) { + + // In this basic test, we'll need a third node, Carol, so we can + // forward a payment through the channel we'll open with the different + // fee policies. + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNode("Bob", nil) + carol := ht.NewNodeWithCoins("Carol", nil) + + ht.EnsureConnected(alice, bob) + ht.EnsureConnected(alice, carol) + + nodes := []*node.HarnessNode{alice, bob, carol} + + // Create a channel Alice->Bob. + chanPoint := ht.OpenChannel(alice, bob, chanParams) + + // Create a channel Carol->Alice. + ht.OpenChannel( + carol, alice, lntest.OpenChannelParams{ + Amt: 500000, + }, + ) + + // Alice and Bob should see each other's ChannelUpdates, advertising + // the preferred routing policies. + assertNodesPolicyUpdate( + ht, nodes, alice, alicePolicy, chanPoint, + ) + assertNodesPolicyUpdate(ht, nodes, bob, bobPolicy, chanPoint) + + // They should now know about the default policies. + for _, n := range nodes { + ht.AssertChannelPolicy( + n, alice.PubKeyStr, alicePolicy, chanPoint, + ) + ht.AssertChannelPolicy( + n, bob.PubKeyStr, bobPolicy, chanPoint, + ) + } + + // We should be able to forward a payment from Carol to Bob + // through the new channel we opened. + payReqs, _, _ := ht.CreatePayReqs(bob, paymentAmt, 1) + ht.CompletePaymentRequests(carol, payReqs) } // testBasicChannelCreationAndUpdates tests multiple channel opening and From 6b84e60db4567a2628621cad41872e20e25b4471 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 9 Nov 2024 00:59:15 +0800 Subject: [PATCH 130/153] itest: break down payment failed tests --- itest/list_on_test.go | 8 +++++++ itest/lnd_payment_test.go | 50 +++++++++++++++++++-------------------- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/itest/list_on_test.go b/itest/list_on_test.go index b4642f3b68..11a2c751fa 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -648,6 +648,10 @@ var allTestCases = []*lntest.TestCase{ Name: "payment failed htlc local swept", TestFunc: testPaymentFailedHTLCLocalSwept, }, + { + Name: "payment failed htlc local swept resumed", + TestFunc: testPaymentFailedHTLCLocalSweptResumed, + }, { Name: "payment succeeded htlc remote swept", TestFunc: testPaymentSucceededHTLCRemoteSwept, @@ -656,6 +660,10 @@ var allTestCases = []*lntest.TestCase{ Name: "send to route failed htlc timeout", TestFunc: testSendToRouteFailHTLCTimeout, }, + { + Name: "send to route failed htlc timeout resumed", + TestFunc: testSendToRouteFailHTLCTimeoutResumed, + }, { Name: "debuglevel show", TestFunc: testDebuglevelShow, diff --git a/itest/lnd_payment_test.go b/itest/lnd_payment_test.go index 8f9b2bc748..72a2c16030 100644 --- a/itest/lnd_payment_test.go +++ b/itest/lnd_payment_test.go @@ -179,21 +179,19 @@ func testPaymentSucceededHTLCRemoteSwept(ht *lntest.HarnessTest) { // out and claimed onchain via the timeout path, the payment will be marked as // failed. This test creates a topology from Alice -> Bob, and let Alice send // payments to Bob. Bob then goes offline, such that Alice's outgoing HTLC will -// time out. Alice will also be restarted to make sure resumed payments are -// also marked as failed. +// time out. func testPaymentFailedHTLCLocalSwept(ht *lntest.HarnessTest) { - success := ht.Run("fail payment", func(t *testing.T) { - st := ht.Subtest(t) - runTestPaymentHTLCTimeout(st, false) - }) - if !success { - return - } + runTestPaymentHTLCTimeout(ht, false) +} - ht.Run("fail resumed payment", func(t *testing.T) { - st := ht.Subtest(t) - runTestPaymentHTLCTimeout(st, true) - }) +// testPaymentFailedHTLCLocalSweptResumed checks that when an outgoing HTLC is +// timed out and claimed onchain via the timeout path, the payment will be +// marked as failed. This test creates a topology from Alice -> Bob, and let +// Alice send payments to Bob. Bob then goes offline, such that Alice's +// outgoing HTLC will time out. Alice will be restarted to make sure resumed +// payments are also marked as failed. +func testPaymentFailedHTLCLocalSweptResumed(ht *lntest.HarnessTest) { + runTestPaymentHTLCTimeout(ht, true) } // runTestPaymentHTLCTimeout is the helper function that actually runs the @@ -1181,21 +1179,21 @@ func sendPaymentInterceptAndCancel(ht *lntest.HarnessTest, // out and claimed onchain via the timeout path, the payment will be marked as // failed. This test creates a topology from Alice -> Bob, and let Alice send // payments to Bob. Bob then goes offline, such that Alice's outgoing HTLC will -// time out. Alice will also be restarted to make sure resumed payments are -// also marked as failed. +// time out. func testSendToRouteFailHTLCTimeout(ht *lntest.HarnessTest) { - success := ht.Run("fail payment", func(t *testing.T) { - st := ht.Subtest(t) - runSendToRouteFailHTLCTimeout(st, false) - }) - if !success { - return - } + runSendToRouteFailHTLCTimeout(ht, false) +} - ht.Run("fail resumed payment", func(t *testing.T) { - st := ht.Subtest(t) - runTestPaymentHTLCTimeout(st, true) - }) +// testSendToRouteFailHTLCTimeout is similar to +// testPaymentFailedHTLCLocalSwept. The only difference is the `SendPayment` is +// replaced with `SendToRouteV2`. It checks that when an outgoing HTLC is timed +// out and claimed onchain via the timeout path, the payment will be marked as +// failed. This test creates a topology from Alice -> Bob, and let Alice send +// payments to Bob. Bob then goes offline, such that Alice's outgoing HTLC will +// time out. Alice will be restarted to make sure resumed payments are also +// marked as failed. +func testSendToRouteFailHTLCTimeoutResumed(ht *lntest.HarnessTest) { + runTestPaymentHTLCTimeout(ht, true) } // runSendToRouteFailHTLCTimeout is the helper function that actually runs the From 27b04890cfbb531dd99da75b6750dbf2673f4422 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 9 Nov 2024 01:12:49 +0800 Subject: [PATCH 131/153] itest: break down channel backup restore tests --- itest/list_on_test.go | 4 - itest/lnd_channel_backup_test.go | 351 +++++++++++++++---------------- 2 files changed, 168 insertions(+), 187 deletions(-) diff --git a/itest/list_on_test.go b/itest/list_on_test.go index 11a2c751fa..3aab57aade 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -19,10 +19,6 @@ var allTestCases = []*lntest.TestCase{ Name: "external channel funding", TestFunc: testExternalFundingChanPoint, }, - { - Name: "channel backup restore basic", - TestFunc: testChannelBackupRestoreBasic, - }, { Name: "channel backup restore unconfirmed", TestFunc: testChannelBackupRestoreUnconfirmed, diff --git a/itest/lnd_channel_backup_test.go b/itest/lnd_channel_backup_test.go index 2a85501cf7..5dc5a1729d 100644 --- a/itest/lnd_channel_backup_test.go +++ b/itest/lnd_channel_backup_test.go @@ -85,6 +85,26 @@ var channelRestoreTestCases = []*lntest.TestCase{ ) }, }, + { + Name: "channel backup restore from rpc", + TestFunc: testChannelBackupRestoreFromRPC, + }, + { + Name: "channel backup restore from file", + TestFunc: testChannelBackupRestoreFromFile, + }, + { + Name: "channel backup restore during creation", + TestFunc: testChannelBackupRestoreDuringCreation, + }, + { + Name: "channel backup restore during unlock", + TestFunc: testChannelBackupRestoreDuringUnlock, + }, + { + Name: "channel backup restore twice", + TestFunc: testChannelBackupRestoreTwice, + }, } type ( @@ -298,202 +318,167 @@ func (c *chanRestoreScenario) testScenario(ht *lntest.HarnessTest, ) } -// testChannelBackupRestore tests that we're able to recover from, and initiate -// the DLP protocol via: the RPC restore command, restoring on unlock, and -// restoring from initial wallet creation. We'll also alternate between -// restoring form the on disk file, and restoring from the exported RPC command -// as well. -func testChannelBackupRestoreBasic(ht *lntest.HarnessTest) { - var testCases = []struct { - name string - restoreMethod restoreMethodType - }{ - // Restore from backups obtained via the RPC interface. Dave - // was the initiator, of the non-advertised channel. - { - name: "restore from RPC backup", - restoreMethod: func(st *lntest.HarnessTest, - oldNode *node.HarnessNode, - backupFilePath string, - password []byte, - mnemonic []string) nodeRestorer { - - // For this restoration method, we'll grab the - // current multi-channel backup from the old - // node, and use it to restore a new node - // within the closure. - chanBackup := oldNode.RPC.ExportAllChanBackups() - - multi := chanBackup.MultiChanBackup. - MultiChanBackup - - // In our nodeRestorer function, we'll restore - // the node from seed, then manually recover - // the channel backup. - return chanRestoreViaRPC( - st, password, mnemonic, multi, - ) - }, - }, +// testChannelBackupRestoreFromRPC tests that we're able to recover from, and +// initiate the DLP protocol via the RPC restore command. +func testChannelBackupRestoreFromRPC(ht *lntest.HarnessTest) { + // Restore from backups obtained via the RPC interface. Dave was the + // initiator, of the non-advertised channel. + restoreMethod := func(st *lntest.HarnessTest, oldNode *node.HarnessNode, + backupFilePath string, password []byte, + mnemonic []string) nodeRestorer { - // Restore the backup from the on-disk file, using the RPC - // interface. - { - name: "restore from backup file", - restoreMethod: func(st *lntest.HarnessTest, - oldNode *node.HarnessNode, - backupFilePath string, - password []byte, - mnemonic []string) nodeRestorer { - - // Read the entire Multi backup stored within - // this node's channel.backup file. - multi, err := os.ReadFile(backupFilePath) - require.NoError(st, err) - - // Now that we have Dave's backup file, we'll - // create a new nodeRestorer that will restore - // using the on-disk channel.backup. - return chanRestoreViaRPC( - st, password, mnemonic, multi, - ) - }, - }, + // For this restoration method, we'll grab the current + // multi-channel backup from the old node, and use it to + // restore a new node within the closure. + chanBackup := oldNode.RPC.ExportAllChanBackups() - // Restore the backup as part of node initialization with the - // prior mnemonic and new backup seed. - { - name: "restore during creation", - restoreMethod: func(st *lntest.HarnessTest, - oldNode *node.HarnessNode, - backupFilePath string, - password []byte, - mnemonic []string) nodeRestorer { - - // First, fetch the current backup state as is, - // to obtain our latest Multi. - chanBackup := oldNode.RPC.ExportAllChanBackups() - backupSnapshot := &lnrpc.ChanBackupSnapshot{ - MultiChanBackup: chanBackup. - MultiChanBackup, - } + multi := chanBackup.MultiChanBackup. + MultiChanBackup - // Create a new nodeRestorer that will restore - // the node using the Multi backup we just - // obtained above. - return func() *node.HarnessNode { - return st.RestoreNodeWithSeed( - "dave", nil, password, mnemonic, - "", revocationWindow, - backupSnapshot, - ) - } - }, - }, + // In our nodeRestorer function, we'll restore the node from + // seed, then manually recover the channel backup. + return chanRestoreViaRPC( + st, password, mnemonic, multi, + ) + } - // Restore the backup once the node has already been - // re-created, using the Unlock call. - { - name: "restore during unlock", - restoreMethod: func(st *lntest.HarnessTest, - oldNode *node.HarnessNode, - backupFilePath string, - password []byte, - mnemonic []string) nodeRestorer { - - // First, fetch the current backup state as is, - // to obtain our latest Multi. - chanBackup := oldNode.RPC.ExportAllChanBackups() - backupSnapshot := &lnrpc.ChanBackupSnapshot{ - MultiChanBackup: chanBackup. - MultiChanBackup, - } + runChanRestoreScenarioBasic(ht, restoreMethod) +} - // Create a new nodeRestorer that will restore - // the node with its seed, but no channel - // backup, shutdown this initialized node, then - // restart it again using Unlock. - return func() *node.HarnessNode { - newNode := st.RestoreNodeWithSeed( - "dave", nil, password, mnemonic, - "", revocationWindow, nil, - ) - st.RestartNodeWithChanBackups( - newNode, backupSnapshot, - ) - - return newNode - } - }, - }, +// testChannelBackupRestoreFromFile tests that we're able to recover from, and +// initiate the DLP protocol via the backup file. +func testChannelBackupRestoreFromFile(ht *lntest.HarnessTest) { + // Restore the backup from the on-disk file, using the RPC interface. + restoreMethod := func(st *lntest.HarnessTest, oldNode *node.HarnessNode, + backupFilePath string, password []byte, + mnemonic []string) nodeRestorer { - // Restore the backup from the on-disk file a second time to - // make sure imports can be canceled and later resumed. - { - name: "restore from backup file twice", - restoreMethod: func(st *lntest.HarnessTest, - oldNode *node.HarnessNode, - backupFilePath string, - password []byte, - mnemonic []string) nodeRestorer { - - // Read the entire Multi backup stored within - // this node's channel.backup file. - multi, err := os.ReadFile(backupFilePath) - require.NoError(st, err) - - // Now that we have Dave's backup file, we'll - // create a new nodeRestorer that will restore - // using the on-disk channel.backup. - // - //nolint:lll - backup := &lnrpc.RestoreChanBackupRequest_MultiChanBackup{ - MultiChanBackup: multi, - } + // Read the entire Multi backup stored within this node's + // channel.backup file. + multi, err := os.ReadFile(backupFilePath) + require.NoError(st, err) + + // Now that we have Dave's backup file, we'll create a new + // nodeRestorer that will restore using the on-disk + // channel.backup. + return chanRestoreViaRPC( + st, password, mnemonic, multi, + ) + } - return func() *node.HarnessNode { - newNode := st.RestoreNodeWithSeed( - "dave", nil, password, mnemonic, - "", revocationWindow, nil, - ) + runChanRestoreScenarioBasic(ht, restoreMethod) +} - req := &lnrpc.RestoreChanBackupRequest{ - Backup: backup, - } - res := newNode.RPC.RestoreChanBackups( - req, - ) - require.EqualValues( - st, 1, res.NumRestored, - ) - - req = &lnrpc.RestoreChanBackupRequest{ - Backup: backup, - } - res = newNode.RPC.RestoreChanBackups( - req, - ) - require.EqualValues( - st, 0, res.NumRestored, - ) - - return newNode - } - }, - }, +// testChannelBackupRestoreFromFile tests that we're able to recover from, and +// initiate the DLP protocol via restoring from initial wallet creation. +func testChannelBackupRestoreDuringCreation(ht *lntest.HarnessTest) { + // Restore the backup as part of node initialization with the prior + // mnemonic and new backup seed. + restoreMethod := func(st *lntest.HarnessTest, oldNode *node.HarnessNode, + backupFilePath string, password []byte, + mnemonic []string) nodeRestorer { + + // First, fetch the current backup state as is, to obtain our + // latest Multi. + chanBackup := oldNode.RPC.ExportAllChanBackups() + backupSnapshot := &lnrpc.ChanBackupSnapshot{ + MultiChanBackup: chanBackup. + MultiChanBackup, + } + + // Create a new nodeRestorer that will restore the node using + // the Multi backup we just obtained above. + return func() *node.HarnessNode { + return st.RestoreNodeWithSeed( + "dave", nil, password, mnemonic, + "", revocationWindow, + backupSnapshot, + ) + } + } + + runChanRestoreScenarioBasic(ht, restoreMethod) +} + +// testChannelBackupRestoreFromFile tests that we're able to recover from, and +// initiate the DLP protocol via restoring on unlock. +func testChannelBackupRestoreDuringUnlock(ht *lntest.HarnessTest) { + // Restore the backup once the node has already been re-created, using + // the Unlock call. + restoreMethod := func(st *lntest.HarnessTest, oldNode *node.HarnessNode, + backupFilePath string, password []byte, + mnemonic []string) nodeRestorer { + + // First, fetch the current backup state as is, to obtain our + // latest Multi. + chanBackup := oldNode.RPC.ExportAllChanBackups() + backupSnapshot := &lnrpc.ChanBackupSnapshot{ + MultiChanBackup: chanBackup. + MultiChanBackup, + } + + // Create a new nodeRestorer that will restore the node with + // its seed, but no channel backup, shutdown this initialized + // node, then restart it again using Unlock. + return func() *node.HarnessNode { + newNode := st.RestoreNodeWithSeed( + "dave", nil, password, mnemonic, + "", revocationWindow, nil, + ) + st.RestartNodeWithChanBackups( + newNode, backupSnapshot, + ) + + return newNode + } } - for _, testCase := range testCases { - tc := testCase - success := ht.Run(tc.name, func(t *testing.T) { - h := ht.Subtest(t) + runChanRestoreScenarioBasic(ht, restoreMethod) +} + +// testChannelBackupRestoreTwice tests that we're able to recover from, and +// initiate the DLP protocol twice by alternating between restoring form the on +// disk file, and restoring from the exported RPC command. +func testChannelBackupRestoreTwice(ht *lntest.HarnessTest) { + // Restore the backup from the on-disk file a second time to make sure + // imports can be canceled and later resumed. + restoreMethod := func(st *lntest.HarnessTest, oldNode *node.HarnessNode, + backupFilePath string, password []byte, + mnemonic []string) nodeRestorer { + + // Read the entire Multi backup stored within this node's + // channel.backup file. + multi, err := os.ReadFile(backupFilePath) + require.NoError(st, err) + + // Now that we have Dave's backup file, we'll create a new + // nodeRestorer that will restore using the on-disk + // channel.backup. + backup := &lnrpc.RestoreChanBackupRequest_MultiChanBackup{ + MultiChanBackup: multi, + } + + return func() *node.HarnessNode { + newNode := st.RestoreNodeWithSeed( + "dave", nil, password, mnemonic, + "", revocationWindow, nil, + ) + + req := &lnrpc.RestoreChanBackupRequest{ + Backup: backup, + } + newNode.RPC.RestoreChanBackups(req) + + req = &lnrpc.RestoreChanBackupRequest{ + Backup: backup, + } + newNode.RPC.RestoreChanBackups(req) - runChanRestoreScenarioBasic(h, tc.restoreMethod) - }) - if !success { - break + return newNode } } + + runChanRestoreScenarioBasic(ht, restoreMethod) } // runChanRestoreScenarioBasic executes a given test case from end to end, From 7a08b781b9262bcd56387969a9f16da396b4c818 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 9 Nov 2024 01:19:43 +0800 Subject: [PATCH 132/153] itest: break down wallet import account tests --- itest/list_on_test.go | 5 +- itest/lnd_wallet_import_test.go | 108 +++++++++++++++----------------- 2 files changed, 50 insertions(+), 63 deletions(-) diff --git a/itest/list_on_test.go b/itest/list_on_test.go index 3aab57aade..f47a23f97c 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -484,10 +484,6 @@ var allTestCases = []*lntest.TestCase{ Name: "simple taproot channel activation", TestFunc: testSimpleTaprootChannelActivation, }, - { - Name: "wallet import account", - TestFunc: testWalletImportAccount, - }, { Name: "wallet import pubkey", TestFunc: testWalletImportPubKey, @@ -676,4 +672,5 @@ func init() { allTestCases = append(allTestCases, fundUtxoSelectionTestCases...) allTestCases = append(allTestCases, zeroConfPolicyTestCases...) allTestCases = append(allTestCases, channelFeePolicyTestCases...) + allTestCases = append(allTestCases, walletImportAccountTestCases...) } diff --git a/itest/lnd_wallet_import_test.go b/itest/lnd_wallet_import_test.go index eab0f2c04f..deb8994687 100644 --- a/itest/lnd_wallet_import_test.go +++ b/itest/lnd_wallet_import_test.go @@ -23,6 +23,55 @@ import ( "github.com/stretchr/testify/require" ) +// walletImportAccountTestCases tests that an imported account can fund +// transactions and channels through PSBTs, by having one node (the one with +// the imported account) craft the transactions and another node act as the +// signer. +// +//nolint:lll +var walletImportAccountTestCases = []*lntest.TestCase{ + { + Name: "wallet import account standard BIP-0044", + TestFunc: func(ht *lntest.HarnessTest) { + testWalletImportAccountScenario( + ht, walletrpc.AddressType_WITNESS_PUBKEY_HASH, + ) + }, + }, + { + Name: "wallet import account standard BIP-0049", + TestFunc: func(ht *lntest.HarnessTest) { + testWalletImportAccountScenario( + ht, walletrpc.AddressType_NESTED_WITNESS_PUBKEY_HASH, + ) + }, + }, + { + Name: "wallet import account lnd BIP-0049 variant", + TestFunc: func(ht *lntest.HarnessTest) { + testWalletImportAccountScenario( + ht, walletrpc.AddressType_HYBRID_NESTED_WITNESS_PUBKEY_HASH, + ) + }, + }, + { + Name: "wallet import account standard BIP-0084", + TestFunc: func(ht *lntest.HarnessTest) { + testWalletImportAccountScenario( + ht, walletrpc.AddressType_WITNESS_PUBKEY_HASH, + ) + }, + }, + { + Name: "wallet import account standard BIP-0086", + TestFunc: func(ht *lntest.HarnessTest) { + testWalletImportAccountScenario( + ht, walletrpc.AddressType_TAPROOT_PUBKEY, + ) + }, + }, +} + const ( defaultAccount = lnwallet.DefaultAccountName defaultImportedAccount = waddrmgr.ImportedAddrAccountName @@ -452,65 +501,6 @@ func fundChanAndCloseFromImportedAccount(ht *lntest.HarnessTest, srcNode, } } -// testWalletImportAccount tests that an imported account can fund transactions -// and channels through PSBTs, by having one node (the one with the imported -// account) craft the transactions and another node act as the signer. -func testWalletImportAccount(ht *lntest.HarnessTest) { - testCases := []struct { - name string - addrType walletrpc.AddressType - }{ - { - name: "standard BIP-0044", - addrType: walletrpc.AddressType_WITNESS_PUBKEY_HASH, - }, - { - name: "standard BIP-0049", - addrType: walletrpc. - AddressType_NESTED_WITNESS_PUBKEY_HASH, - }, - { - name: "lnd BIP-0049 variant", - addrType: walletrpc. - AddressType_HYBRID_NESTED_WITNESS_PUBKEY_HASH, - }, - { - name: "standard BIP-0084", - addrType: walletrpc.AddressType_WITNESS_PUBKEY_HASH, - }, - { - name: "standard BIP-0086", - addrType: walletrpc.AddressType_TAPROOT_PUBKEY, - }, - } - - for _, tc := range testCases { - tc := tc - success := ht.Run(tc.name, func(tt *testing.T) { - testFunc := func(ht *lntest.HarnessTest) { - testWalletImportAccountScenario( - ht, tc.addrType, - ) - } - - st := ht.Subtest(tt) - - st.RunTestCase(&lntest.TestCase{ - Name: tc.name, - TestFunc: testFunc, - }) - }) - if !success { - // Log failure time to help relate the lnd logs to the - // failure. - ht.Logf("Failure time: %v", time.Now().Format( - "2006-01-02 15:04:05.000", - )) - break - } - } -} - func testWalletImportAccountScenario(ht *lntest.HarnessTest, addrType walletrpc.AddressType) { From 41d12034b1c45f77a85ee7e781fc5c5c03550037 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 9 Nov 2024 14:56:57 +0800 Subject: [PATCH 133/153] itest: break down basic funding flow tests --- itest/list_on_test.go | 5 +- itest/lnd_funding_test.go | 332 +++++++++++++++++++++++--------------- 2 files changed, 201 insertions(+), 136 deletions(-) diff --git a/itest/list_on_test.go b/itest/list_on_test.go index f47a23f97c..17e48a8669 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -11,10 +11,6 @@ var allTestCases = []*lntest.TestCase{ Name: "update channel status", TestFunc: testUpdateChanStatus, }, - { - Name: "basic funding flow", - TestFunc: testBasicChannelFunding, - }, { Name: "external channel funding", TestFunc: testExternalFundingChanPoint, @@ -673,4 +669,5 @@ func init() { allTestCases = append(allTestCases, zeroConfPolicyTestCases...) allTestCases = append(allTestCases, channelFeePolicyTestCases...) allTestCases = append(allTestCases, walletImportAccountTestCases...) + allTestCases = append(allTestCases, basicFundingTestCases...) } diff --git a/itest/lnd_funding_test.go b/itest/lnd_funding_test.go index adb56fa123..1aefc360ce 100644 --- a/itest/lnd_funding_test.go +++ b/itest/lnd_funding_test.go @@ -25,162 +25,230 @@ import ( "github.com/stretchr/testify/require" ) -// testBasicChannelFunding performs a test exercising expected behavior from a -// basic funding workflow. The test creates a new channel between Alice and -// Bob, then immediately closes the channel after asserting some expected post -// conditions. Finally, the chain itself is checked to ensure the closing -// transaction was mined. -func testBasicChannelFunding(ht *lntest.HarnessTest) { - // Run through the test with combinations of all the different - // commitment types. - allTypes := []lnrpc.CommitmentType{ - lnrpc.CommitmentType_STATIC_REMOTE_KEY, - lnrpc.CommitmentType_ANCHORS, - lnrpc.CommitmentType_SIMPLE_TAPROOT, - } +// basicFundingTestCases defines the test cases for the basic funding test. +var basicFundingTestCases = []*lntest.TestCase{ + { + Name: "basic funding flow static key remote", + TestFunc: testBasicChannelFundingStaticRemote, + }, + { + Name: "basic funding flow anchor", + TestFunc: testBasicChannelFundingAnchor, + }, + { + Name: "basic funding flow simple taproot", + TestFunc: testBasicChannelFundingSimpleTaproot, + }, +} + +// allFundingTypes defines the channel types to test for the basic funding +// test. +var allFundingTypes = []lnrpc.CommitmentType{ + lnrpc.CommitmentType_STATIC_REMOTE_KEY, + lnrpc.CommitmentType_ANCHORS, + lnrpc.CommitmentType_SIMPLE_TAPROOT, +} - // testFunding is a function closure that takes Carol and Dave's - // commitment types and test the funding flow. - testFunding := func(ht *lntest.HarnessTest, carolCommitType, - daveCommitType lnrpc.CommitmentType) { +// testBasicChannelFundingStaticRemote performs a test exercising expected +// behavior from a basic funding workflow. The test creates a new channel +// between Carol and Dave, with Carol using the static remote key commitment +// type, and Dave using allFundingTypes. +func testBasicChannelFundingStaticRemote(ht *lntest.HarnessTest) { + carolCommitType := lnrpc.CommitmentType_STATIC_REMOTE_KEY - // Based on the current tweak variable for Carol, we'll - // preferentially signal the legacy commitment format. We do - // the same for Dave shortly below. - carolArgs := lntest.NodeArgsForCommitType(carolCommitType) - carol := ht.NewNode("Carol", carolArgs) + // We'll test all possible combinations of the feature bit presence + // that both nodes can signal for this new channel type. We'll make a + // new Carol+Dave for each test instance as well. + for _, daveCommitType := range allFundingTypes { + cc := carolCommitType + dc := daveCommitType - // Each time, we'll send Carol a new set of coins in order to - // fund the channel. - ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) + testName := fmt.Sprintf( + "carol_commit=%v,dave_commit=%v", cc, dc, + ) - daveArgs := lntest.NodeArgsForCommitType(daveCommitType) - dave := ht.NewNode("Dave", daveArgs) + success := ht.Run(testName, func(t *testing.T) { + st := ht.Subtest(t) + runBasicFundingTest(st, cc, dc) + }) - // Before we start the test, we'll ensure both sides are - // connected to the funding flow can properly be executed. - ht.EnsureConnected(carol, dave) + if !success { + break + } + } +} - var privateChan bool +// testBasicChannelFundingAnchor performs a test exercising expected behavior +// from a basic funding workflow. The test creates a new channel between Carol +// and Dave, with Carol using the anchor commitment type, and Dave using +// allFundingTypes. +func testBasicChannelFundingAnchor(ht *lntest.HarnessTest) { + carolCommitType := lnrpc.CommitmentType_ANCHORS - // If this is to be a taproot channel type, then it needs to be - // private, otherwise it'll be rejected by Dave. - // - // TODO(roasbeef): lift after gossip 1.75 - if carolCommitType == lnrpc.CommitmentType_SIMPLE_TAPROOT { - privateChan = true + // We'll test all possible combinations of the feature bit presence + // that both nodes can signal for this new channel type. We'll make a + // new Carol+Dave for each test instance as well. + for _, daveCommitType := range allFundingTypes { + cc := carolCommitType + dc := daveCommitType + + testName := fmt.Sprintf( + "carol_commit=%v,dave_commit=%v", cc, dc, + ) + + success := ht.Run(testName, func(t *testing.T) { + st := ht.Subtest(t) + runBasicFundingTest(st, cc, dc) + }) + + if !success { + break } + } +} + +// testBasicChannelFundingSimpleTaproot performs a test exercising expected +// behavior from a basic funding workflow. The test creates a new channel +// between Carol and Dave, with Carol using the simple taproot commitment type, +// and Dave using allFundingTypes. +func testBasicChannelFundingSimpleTaproot(ht *lntest.HarnessTest) { + carolCommitType := lnrpc.CommitmentType_SIMPLE_TAPROOT + + // We'll test all possible combinations of the feature bit presence + // that both nodes can signal for this new channel type. We'll make a + // new Carol+Dave for each test instance as well. + for _, daveCommitType := range allFundingTypes { + cc := carolCommitType + dc := daveCommitType + + testName := fmt.Sprintf( + "carol_commit=%v,dave_commit=%v", cc, dc, + ) + + success := ht.Run(testName, func(t *testing.T) { + st := ht.Subtest(t) + runBasicFundingTest(st, cc, dc) + }) - // If carol wants taproot, but dave wants something - // else, then we'll assert that the channel negotiation - // attempt fails. - if carolCommitType == lnrpc.CommitmentType_SIMPLE_TAPROOT && - daveCommitType != lnrpc.CommitmentType_SIMPLE_TAPROOT { - - expectedErr := fmt.Errorf("requested channel type " + - "not supported") - amt := funding.MaxBtcFundingAmount - ht.OpenChannelAssertErr( - carol, dave, lntest.OpenChannelParams{ - Private: privateChan, - Amt: amt, - CommitmentType: carolCommitType, - }, expectedErr, - ) - - return + if !success { + break } + } +} + +// runBasicFundingTest is a helper function that takes Carol and Dave's +// commitment types and test the funding flow. +func runBasicFundingTest(ht *lntest.HarnessTest, carolCommitType, + daveCommitType lnrpc.CommitmentType) { + + // Based on the current tweak variable for Carol, we'll preferentially + // signal the legacy commitment format. We do the same for Dave + // shortly below. + carolArgs := lntest.NodeArgsForCommitType(carolCommitType) + carol := ht.NewNode("Carol", carolArgs) + + // Each time, we'll send Carol a new set of coins in order to fund the + // channel. + ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) - carolChan, daveChan := basicChannelFundingTest( - ht, carol, dave, nil, privateChan, &carolCommitType, + daveArgs := lntest.NodeArgsForCommitType(daveCommitType) + dave := ht.NewNode("Dave", daveArgs) + + // Before we start the test, we'll ensure both sides are connected to + // the funding flow can properly be executed. + ht.EnsureConnected(carol, dave) + + var privateChan bool + + // If this is to be a taproot channel type, then it needs to be + // private, otherwise it'll be rejected by Dave. + // + // TODO(roasbeef): lift after gossip 1.75 + if carolCommitType == lnrpc.CommitmentType_SIMPLE_TAPROOT { + privateChan = true + } + + // If carol wants taproot, but dave wants something else, then we'll + // assert that the channel negotiation attempt fails. + if carolCommitType == lnrpc.CommitmentType_SIMPLE_TAPROOT && + daveCommitType != lnrpc.CommitmentType_SIMPLE_TAPROOT { + + expectedErr := fmt.Errorf("requested channel type " + + "not supported") + amt := funding.MaxBtcFundingAmount + ht.OpenChannelAssertErr( + carol, dave, lntest.OpenChannelParams{ + Private: privateChan, + Amt: amt, + CommitmentType: carolCommitType, + }, expectedErr, ) - // Both nodes should report the same commitment - // type. - chansCommitType := carolChan.CommitmentType - require.Equal(ht, chansCommitType, daveChan.CommitmentType, - "commit types don't match") + return + } - // Now check that the commitment type reported by both nodes is - // what we expect. It will be the minimum of the two nodes' - // preference, in the order Legacy, Tweakless, Anchors. - expType := carolCommitType + carolChan, daveChan := basicChannelFundingTest( + ht, carol, dave, nil, privateChan, &carolCommitType, + ) - switch daveCommitType { - // Dave supports taproot, type will be what Carol supports. - case lnrpc.CommitmentType_SIMPLE_TAPROOT: + // Both nodes should report the same commitment type. + chansCommitType := carolChan.CommitmentType + require.Equal(ht, chansCommitType, daveChan.CommitmentType, + "commit types don't match") + + // Now check that the commitment type reported by both nodes is what we + // expect. It will be the minimum of the two nodes' preference, in the + // order Legacy, Tweakless, Anchors. + expType := carolCommitType + + switch daveCommitType { + // Dave supports taproot, type will be what Carol supports. + case lnrpc.CommitmentType_SIMPLE_TAPROOT: + + // Dave supports anchors, type will be what Carol supports. + case lnrpc.CommitmentType_ANCHORS: + // However if Alice wants taproot chans, then we downgrade to + // anchors as this is still using implicit negotiation. + if expType == lnrpc.CommitmentType_SIMPLE_TAPROOT { + expType = lnrpc.CommitmentType_ANCHORS + } - // Dave supports anchors, type will be what Carol supports. + // Dave only supports tweakless, channel will be downgraded to this + // type if Carol supports anchors. + case lnrpc.CommitmentType_STATIC_REMOTE_KEY: + switch expType { case lnrpc.CommitmentType_ANCHORS: - // However if Alice wants taproot chans, then we - // downgrade to anchors as this is still using implicit - // negotiation. - if expType == lnrpc.CommitmentType_SIMPLE_TAPROOT { - expType = lnrpc.CommitmentType_ANCHORS - } - - // Dave only supports tweakless, channel will be downgraded to - // this type if Carol supports anchors. - case lnrpc.CommitmentType_STATIC_REMOTE_KEY: - switch expType { - case lnrpc.CommitmentType_ANCHORS: - expType = lnrpc.CommitmentType_STATIC_REMOTE_KEY - case lnrpc.CommitmentType_SIMPLE_TAPROOT: - expType = lnrpc.CommitmentType_STATIC_REMOTE_KEY - } - - // Dave only supports legacy type, channel will be downgraded - // to this type. - case lnrpc.CommitmentType_LEGACY: - expType = lnrpc.CommitmentType_LEGACY - - default: - ht.Fatalf("invalid commit type %v", daveCommitType) + expType = lnrpc.CommitmentType_STATIC_REMOTE_KEY + case lnrpc.CommitmentType_SIMPLE_TAPROOT: + expType = lnrpc.CommitmentType_STATIC_REMOTE_KEY } - // Check that the signalled type matches what we expect. - switch { - case expType == lnrpc.CommitmentType_ANCHORS && - chansCommitType == lnrpc.CommitmentType_ANCHORS: + // Dave only supports legacy type, channel will be downgraded to this + // type. + case lnrpc.CommitmentType_LEGACY: + expType = lnrpc.CommitmentType_LEGACY - case expType == lnrpc.CommitmentType_STATIC_REMOTE_KEY && - chansCommitType == lnrpc.CommitmentType_STATIC_REMOTE_KEY: //nolint:lll + default: + ht.Fatalf("invalid commit type %v", daveCommitType) + } - case expType == lnrpc.CommitmentType_LEGACY && - chansCommitType == lnrpc.CommitmentType_LEGACY: + // Check that the signalled type matches what we expect. + switch { + case expType == lnrpc.CommitmentType_ANCHORS && + chansCommitType == lnrpc.CommitmentType_ANCHORS: - case expType == lnrpc.CommitmentType_SIMPLE_TAPROOT && - chansCommitType == lnrpc.CommitmentType_SIMPLE_TAPROOT: + case expType == lnrpc.CommitmentType_STATIC_REMOTE_KEY && + chansCommitType == lnrpc.CommitmentType_STATIC_REMOTE_KEY: - default: - ht.Fatalf("expected nodes to signal "+ - "commit type %v, instead got "+ - "%v", expType, chansCommitType) - } - } + case expType == lnrpc.CommitmentType_LEGACY && + chansCommitType == lnrpc.CommitmentType_LEGACY: -test: - // We'll test all possible combinations of the feature bit presence - // that both nodes can signal for this new channel type. We'll make a - // new Carol+Dave for each test instance as well. - for _, carolCommitType := range allTypes { - for _, daveCommitType := range allTypes { - cc := carolCommitType - dc := daveCommitType - - testName := fmt.Sprintf( - "carol_commit=%v,dave_commit=%v", cc, dc, - ) - - success := ht.Run(testName, func(t *testing.T) { - st := ht.Subtest(t) - testFunding(st, cc, dc) - }) - - if !success { - break test - } - } + case expType == lnrpc.CommitmentType_SIMPLE_TAPROOT && + chansCommitType == lnrpc.CommitmentType_SIMPLE_TAPROOT: + + default: + ht.Fatalf("expected nodes to signal commit type %v, instead "+ + "got %v", expType, chansCommitType) } } From c0d284c2a9b2a23c419af78a4db49730e61818ea Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 9 Nov 2024 15:14:53 +0800 Subject: [PATCH 134/153] itest: break down single hop send to route Also removed the duplicate test cases. --- itest/list_on_test.go | 5 +-- itest/lnd_routing_test.go | 70 +++++++++++++-------------------------- 2 files changed, 24 insertions(+), 51 deletions(-) diff --git a/itest/list_on_test.go b/itest/list_on_test.go index 17e48a8669..319f5dc79c 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -300,10 +300,6 @@ var allTestCases = []*lntest.TestCase{ Name: "revoked uncooperative close retribution remote hodl", TestFunc: testRevokedCloseRetributionRemoteHodl, }, - { - Name: "single-hop send to route", - TestFunc: testSingleHopSendToRoute, - }, { Name: "multi-hop send to route", TestFunc: testMultiHopSendToRoute, @@ -670,4 +666,5 @@ func init() { allTestCases = append(allTestCases, channelFeePolicyTestCases...) allTestCases = append(allTestCases, walletImportAccountTestCases...) allTestCases = append(allTestCases, basicFundingTestCases...) + allTestCases = append(allTestCases, sendToRouteTestCases...) } diff --git a/itest/lnd_routing_test.go b/itest/lnd_routing_test.go index 950b24d24b..c32e021d44 100644 --- a/itest/lnd_routing_test.go +++ b/itest/lnd_routing_test.go @@ -20,46 +20,33 @@ import ( "google.golang.org/protobuf/proto" ) -type singleHopSendToRouteCase struct { - name string - - // streaming tests streaming SendToRoute if true, otherwise tests - // synchronous SenToRoute. - streaming bool - - // routerrpc submits the request to the routerrpc subserver if true, - // otherwise submits to the main rpc server. - routerrpc bool -} - -var singleHopSendToRouteCases = []singleHopSendToRouteCase{ - { - name: "regular main sync", - }, - { - name: "regular main stream", - streaming: true, - }, - { - name: "regular routerrpc sync", - routerrpc: true, - }, +var sendToRouteTestCases = []*lntest.TestCase{ { - name: "mpp main sync", + Name: "single hop send to route sync", + TestFunc: func(ht *lntest.HarnessTest) { + // useStream: false, routerrpc: false. + testSingleHopSendToRouteCase(ht, false, false) + }, }, { - name: "mpp main stream", - streaming: true, + Name: "single hop send to route stream", + TestFunc: func(ht *lntest.HarnessTest) { + // useStream: true, routerrpc: false. + testSingleHopSendToRouteCase(ht, true, false) + }, }, { - name: "mpp routerrpc sync", - routerrpc: true, + Name: "single hop send to route v2", + TestFunc: func(ht *lntest.HarnessTest) { + // useStream: false, routerrpc: true. + testSingleHopSendToRouteCase(ht, false, true) + }, }, } -// testSingleHopSendToRoute tests that payments are properly processed through a -// provided route with a single hop. We'll create the following network -// topology: +// testSingleHopSendToRouteCase tests that payments are properly processed +// through a provided route with a single hop. We'll create the following +// network topology: // // Carol --100k--> Dave // @@ -67,19 +54,8 @@ var singleHopSendToRouteCases = []singleHopSendToRouteCase{ // by feeding the route back into the various SendToRoute RPC methods. Here we // test all three SendToRoute endpoints, forcing each to perform both a regular // payment and an MPP payment. -func testSingleHopSendToRoute(ht *lntest.HarnessTest) { - for _, test := range singleHopSendToRouteCases { - test := test - - ht.Run(test.name, func(t1 *testing.T) { - st := ht.Subtest(t1) - testSingleHopSendToRouteCase(st, test) - }) - } -} - func testSingleHopSendToRouteCase(ht *lntest.HarnessTest, - test singleHopSendToRouteCase) { + useStream, useRPC bool) { const chanAmt = btcutil.Amount(100000) const paymentAmtSat = 1000 @@ -199,11 +175,11 @@ func testSingleHopSendToRouteCase(ht *lntest.HarnessTest, // synchronously via the routerrpc's SendToRoute, or via the main RPC // server's SendToRoute streaming or sync calls. switch { - case !test.routerrpc && test.streaming: + case !useRPC && useStream: sendToRouteStream() - case !test.routerrpc && !test.streaming: + case !useRPC && !useStream: sendToRouteSync() - case test.routerrpc && !test.streaming: + case useRPC && !useStream: sendToRouteRouterRPC() default: require.Fail(ht, "routerrpc does not support "+ From c18e35605379b54329678aea4c75b49fbeab18bc Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 9 Nov 2024 15:18:40 +0800 Subject: [PATCH 135/153] itest: break down taproot tests --- itest/list_on_test.go | 12 ++++++++++-- itest/lnd_taproot_test.go | 17 ++++++++++++++--- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/itest/list_on_test.go b/itest/list_on_test.go index 319f5dc79c..7f7bfca07b 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -469,8 +469,16 @@ var allTestCases = []*lntest.TestCase{ TestFunc: testBumpForceCloseFee, }, { - Name: "taproot", - TestFunc: testTaproot, + Name: "taproot spend", + TestFunc: testTaprootSpend, + }, + { + Name: "taproot musig2", + TestFunc: testTaprootMuSig2, + }, + { + Name: "taproot import scripts", + TestFunc: testTaprootImportScripts, }, { Name: "simple taproot channel activation", diff --git a/itest/lnd_taproot_test.go b/itest/lnd_taproot_test.go index 2b68105fdb..ddbfb6983a 100644 --- a/itest/lnd_taproot_test.go +++ b/itest/lnd_taproot_test.go @@ -46,9 +46,9 @@ var ( )) ) -// testTaproot ensures that the daemon can send to and spend from taproot (p2tr) -// outputs. -func testTaproot(ht *lntest.HarnessTest) { +// testTaprootSpend ensures that the daemon can send to and spend from taproot +// (p2tr) outputs. +func testTaprootSpend(ht *lntest.HarnessTest) { alice := ht.NewNode("Alice", nil) testTaprootSendCoinsKeySpendBip86(ht, alice) @@ -62,6 +62,12 @@ func testTaproot(ht *lntest.HarnessTest) { ht, alice, txscript.SigHashSingle, ) testTaprootSignOutputRawKeySpendRootHash(ht, alice) +} + +// testTaprootMuSig2 ensures that the daemon can send to and spend from taproot +// (p2tr) outputs using musig2. +func testTaprootMuSig2(ht *lntest.HarnessTest) { + alice := ht.NewNodeWithCoins("Alice", nil) muSig2Versions := []signrpc.MuSig2Version{ signrpc.MuSig2Version_MUSIG2_VERSION_V040, @@ -74,6 +80,11 @@ func testTaproot(ht *lntest.HarnessTest) { testTaprootMuSig2CombinedLeafKeySpend(ht, alice, version) testMuSig2CombineKey(ht, alice, version) } +} + +// testTaprootImportScripts ensures that the daemon can import taproot scripts. +func testTaprootImportScripts(ht *lntest.HarnessTest) { + alice := ht.NewNodeWithCoins("Alice", nil) testTaprootImportTapscriptFullTree(ht, alice) testTaprootImportTapscriptPartialReveal(ht, alice) From ce88b8be6496d7134b2f742a40b5d75f04b8f7cd Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 9 Nov 2024 15:41:48 +0800 Subject: [PATCH 136/153] itest: break down channel fundmax tests --- itest/list_on_test.go | 12 ++- itest/lnd_channel_funding_fund_max_test.go | 120 +++++++++++++++++---- 2 files changed, 111 insertions(+), 21 deletions(-) diff --git a/itest/list_on_test.go b/itest/list_on_test.go index 7f7bfca07b..cdc976fe1b 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -525,8 +525,16 @@ var allTestCases = []*lntest.TestCase{ TestFunc: testLookupHtlcResolution, }, { - Name: "channel fundmax", - TestFunc: testChannelFundMax, + Name: "channel fundmax error", + TestFunc: testChannelFundMaxError, + }, + { + Name: "channel fundmax wallet amount", + TestFunc: testChannelFundMaxWalletAmount, + }, + { + Name: "channel fundmax anchor reserve", + TestFunc: testChannelFundMaxAnchorReserve, }, { Name: "htlc timeout resolver extract preimage remote", diff --git a/itest/lnd_channel_funding_fund_max_test.go b/itest/lnd_channel_funding_fund_max_test.go index c93ab5fdc7..5a19ccdebd 100644 --- a/itest/lnd_channel_funding_fund_max_test.go +++ b/itest/lnd_channel_funding_fund_max_test.go @@ -50,9 +50,9 @@ type chanFundMaxTestCase struct { private bool } -// testChannelFundMax checks various channel funding scenarios where the user -// instructed the wallet to use all remaining funds. -func testChannelFundMax(ht *lntest.HarnessTest) { +// testChannelFundMaxError checks various error channel funding scenarios where +// the user instructed the wallet to use all remaining funds. +func testChannelFundMaxError(ht *lntest.HarnessTest) { // Create two new nodes that open a channel between each other for these // tests. args := lntest.NodeArgsForCommitType(lnrpc.CommitmentType_ANCHORS) @@ -92,22 +92,6 @@ func testChannelFundMax(ht *lntest.HarnessTest) { expectedErrStr: "available funds(0.00017877 BTC) " + "below the minimum amount(0.00020000 BTC)", }, - { - name: "wallet amount > min chan " + - "size (37000sat)", - initialWalletBalance: 37_000, - // The transaction fee to open the channel must be - // subtracted from Alice's balance. - // (since wallet balance < max-chan-size) - expectedBalanceAlice: btcutil.Amount(37_000) - - fundingFee(1, false), - }, - { - name: "wallet amount > max chan size " + - "(20000000sat)", - initialWalletBalance: 20_000_000, - expectedBalanceAlice: lnd.MaxFundingAmount, - }, // Expects, that if the maximum funding amount for a channel is // pushed to the remote side, then the funding flow is failing // because the push amount has to be less than the local channel @@ -137,6 +121,63 @@ func testChannelFundMax(ht *lntest.HarnessTest) { expectedErrStr: "funder balance too small (-8050000) " + "with fee=9050 sat, minimum=708 sat required", }, + } + + for _, testCase := range testCases { + success := ht.Run( + testCase.name, func(tt *testing.T) { + runFundMaxTestCase( + ht, alice, bob, testCase, reserveAmount, + ) + }, + ) + + // Stop at the first failure. Mimic behavior of original test + // framework. + if !success { + break + } + } +} + +// testChannelFundMaxWalletAmount checks various channel funding scenarios +// where the user instructed the wallet to use all remaining funds and succeed. +func testChannelFundMaxWalletAmount(ht *lntest.HarnessTest) { + // Create two new nodes that open a channel between each other for these + // tests. + args := lntest.NodeArgsForCommitType(lnrpc.CommitmentType_ANCHORS) + alice := ht.NewNode("Alice", args) + bob := ht.NewNode("Bob", args) + + // Ensure both sides are connected so the funding flow can be properly + // executed. + ht.EnsureConnected(alice, bob) + + // Calculate reserve amount for one channel. + reserveResp, _ := alice.RPC.WalletKit.RequiredReserve( + context.Background(), &walletrpc.RequiredReserveRequest{ + AdditionalPublicChannels: 1, + }, + ) + reserveAmount := btcutil.Amount(reserveResp.RequiredReserve) + + var testCases = []*chanFundMaxTestCase{ + { + name: "wallet amount > min chan " + + "size (37000sat)", + initialWalletBalance: 37_000, + // The transaction fee to open the channel must be + // subtracted from Alice's balance. + // (since wallet balance < max-chan-size) + expectedBalanceAlice: btcutil.Amount(37_000) - + fundingFee(1, false), + }, + { + name: "wallet amount > max chan size " + + "(20000000sat)", + initialWalletBalance: 20_000_000, + expectedBalanceAlice: lnd.MaxFundingAmount, + }, { name: "wallet amount > max chan size, " + "push amount 16766000", @@ -144,7 +185,48 @@ func testChannelFundMax(ht *lntest.HarnessTest) { pushAmt: 16_766_000, expectedBalanceAlice: lnd.MaxFundingAmount - 16_766_000, }, + } + for _, testCase := range testCases { + success := ht.Run( + testCase.name, func(tt *testing.T) { + runFundMaxTestCase( + ht, alice, bob, testCase, reserveAmount, + ) + }, + ) + + // Stop at the first failure. Mimic behavior of original test + // framework. + if !success { + break + } + } +} + +// testChannelFundMaxAnchorReserve checks various channel funding scenarios +// where the user instructed the wallet to use all remaining funds and its +// impact on anchor reserve. +func testChannelFundMaxAnchorReserve(ht *lntest.HarnessTest) { + // Create two new nodes that open a channel between each other for these + // tests. + args := lntest.NodeArgsForCommitType(lnrpc.CommitmentType_ANCHORS) + alice := ht.NewNode("Alice", args) + bob := ht.NewNode("Bob", args) + + // Ensure both sides are connected so the funding flow can be properly + // executed. + ht.EnsureConnected(alice, bob) + + // Calculate reserve amount for one channel. + reserveResp, _ := alice.RPC.WalletKit.RequiredReserve( + context.Background(), &walletrpc.RequiredReserveRequest{ + AdditionalPublicChannels: 1, + }, + ) + reserveAmount := btcutil.Amount(reserveResp.RequiredReserve) + + var testCases = []*chanFundMaxTestCase{ { name: "anchor reserved value", initialWalletBalance: 100_000, From aace64e790cf8ff26a9c289474b38f19270211ca Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 9 Nov 2024 14:39:12 +0800 Subject: [PATCH 137/153] itest: further reduce block mined in tests --- itest/lnd_channel_backup_test.go | 1 - itest/lnd_estimate_route_fee_test.go | 36 ++++++++++----------------- itest/lnd_hold_invoice_force_test.go | 2 +- itest/lnd_route_blinding_test.go | 37 ++++++++++++---------------- itest/lnd_wipe_fwdpkgs_test.go | 3 --- 5 files changed, 30 insertions(+), 49 deletions(-) diff --git a/itest/lnd_channel_backup_test.go b/itest/lnd_channel_backup_test.go index 5dc5a1729d..3d3e014355 100644 --- a/itest/lnd_channel_backup_test.go +++ b/itest/lnd_channel_backup_test.go @@ -1499,7 +1499,6 @@ func assertTimeLockSwept(ht *lntest.HarnessTest, carol, dave *node.HarnessNode, ht.AssertNumPendingSweeps(dave, 2) // Mine a block to trigger the sweeps. - ht.MineEmptyBlocks(1) daveSweep := ht.AssertNumTxsInMempool(1)[0] block := ht.MineBlocksAndAssertNumTxes(1, 1)[0] ht.AssertTxInBlock(block, daveSweep) diff --git a/itest/lnd_estimate_route_fee_test.go b/itest/lnd_estimate_route_fee_test.go index e82e51df2e..e33aa6bb8f 100644 --- a/itest/lnd_estimate_route_fee_test.go +++ b/itest/lnd_estimate_route_fee_test.go @@ -93,21 +93,17 @@ func testEstimateRouteFee(ht *lntest.HarnessTest) { // added to the invoice always have enough liquidity, but here we check // that the prober uses the more expensive route. ht.EnsureConnected(mts.bob, paula) - channelPointBobPaula := ht.OpenChannel( - mts.bob, paula, lntest.OpenChannelParams{ - Private: true, - Amt: 90_000, - PushAmt: 69_000, - }, - ) + ht.OpenChannel(mts.bob, paula, lntest.OpenChannelParams{ + Private: true, + Amt: 90_000, + PushAmt: 69_000, + }) ht.EnsureConnected(mts.eve, paula) - channelPointEvePaula := ht.OpenChannel( - mts.eve, paula, lntest.OpenChannelParams{ - Private: true, - Amt: 1_000_000, - }, - ) + ht.OpenChannel(mts.eve, paula, lntest.OpenChannelParams{ + Private: true, + Amt: 1_000_000, + }) bobsPrivChannels := mts.bob.RPC.ListChannels(&lnrpc.ListChannelsRequest{ PrivateOnly: true, @@ -242,6 +238,8 @@ func testEstimateRouteFee(ht *lntest.HarnessTest) { locktime := initialBlockHeight + defaultTimelock + int64(routing.BlockPadding) + noChanNode := ht.NewNode("ImWithoutChannels", nil) + var testCases = []*estimateRouteFeeTestCase{ // Single hop payment is free. { @@ -303,10 +301,8 @@ func testEstimateRouteFee(ht *lntest.HarnessTest) { { name: "single hop hint, destination " + "without channels", - probing: true, - destination: ht.NewNode( - "ImWithoutChannels", nil, - ), + probing: true, + destination: noChanNode, routeHints: singleRouteHint, expectedRoutingFeesMsat: feeACBP, expectedCltvDelta: locktime + deltaACBP, @@ -356,12 +352,6 @@ func testEstimateRouteFee(ht *lntest.HarnessTest) { break } } - - mts.ht.CloseChannelAssertPending(mts.bob, channelPointBobPaula, false) - mts.ht.CloseChannelAssertPending(mts.eve, channelPointEvePaula, false) - ht.MineBlocksAndAssertNumTxes(1, 2) - - mts.closeChannels() } // runTestCase runs a single test case asserting that test conditions are met. diff --git a/itest/lnd_hold_invoice_force_test.go b/itest/lnd_hold_invoice_force_test.go index 19e44a8375..5fe8ea0daa 100644 --- a/itest/lnd_hold_invoice_force_test.go +++ b/itest/lnd_hold_invoice_force_test.go @@ -30,7 +30,7 @@ func testHoldInvoiceForceClose(ht *lntest.HarnessTest) { ) invoiceReq := &invoicesrpc.AddHoldInvoiceRequest{ Value: 30000, - CltvExpiry: 40, + CltvExpiry: finalCltvDelta, Hash: payHash[:], } bobInvoice := bob.RPC.AddHoldInvoice(invoiceReq) diff --git a/itest/lnd_route_blinding_test.go b/itest/lnd_route_blinding_test.go index 1948271814..4b6f98d18e 100644 --- a/itest/lnd_route_blinding_test.go +++ b/itest/lnd_route_blinding_test.go @@ -5,6 +5,7 @@ import ( "crypto/sha256" "encoding/hex" "errors" + "fmt" "time" "github.com/btcsuite/btcd/btcec/v2" @@ -19,6 +20,10 @@ import ( "github.com/stretchr/testify/require" ) +// toLocalCSV is the CSV delay for the node's to_local output. We use a small +// value to save us from mining blocks. +var toLocalCSV = 2 + // testQueryBlindedRoutes tests querying routes to blinded routes. To do this, // it sets up a nework of Alice - Bob - Carol and creates a mock blinded route // that uses Carol as the introduction node (plus dummy hops to cover multiple @@ -346,12 +351,18 @@ func newBlindedForwardTest(ht *lntest.HarnessTest) (context.Context, func (b *blindedForwardTest) setupNetwork(ctx context.Context, withInterceptor bool) { - carolArgs := []string{"--bitcoin.timelockdelta=18"} + carolArgs := []string{ + "--bitcoin.timelockdelta=18", + fmt.Sprintf("--bitcoin.defaultremotedelay=%v", toLocalCSV), + } if withInterceptor { carolArgs = append(carolArgs, "--requireinterceptor") } - daveArgs := []string{"--bitcoin.timelockdelta=18"} + daveArgs := []string{ + "--bitcoin.timelockdelta=18", + fmt.Sprintf("--bitcoin.defaultremotedelay=%v", toLocalCSV), + } cfgs := [][]string{nil, nil, carolArgs, daveArgs} param := lntest.OpenChannelParams{ Amt: chanAmt, @@ -778,11 +789,11 @@ func testErrorHandlingOnChainFailure(ht *lntest.HarnessTest) { // SuspendCarol so that she can't interfere with the resolution of the // HTLC from now on. - restartCarol := ht.SuspendNode(testCase.carol) + ht.SuspendNode(testCase.carol) // Mine blocks so that Bob will claim his CSV delayed local commitment, // we've already mined 1 block so we need one less than our CSV. - ht.MineBlocks(node.DefaultCSV - 1) + ht.MineBlocks(toLocalCSV - 1) ht.AssertNumPendingSweeps(bob, 1) ht.MineBlocksAndAssertNumTxes(1, 1) @@ -795,6 +806,7 @@ func testErrorHandlingOnChainFailure(ht *lntest.HarnessTest) { // value. info := bob.RPC.GetInfo() target := carolHTLC.IncomingExpiry - info.BlockHeight + ht.Log(carolHTLC.IncomingExpiry, info.BlockHeight, target) ht.MineBlocks(int(target)) // Wait for Bob's timeout transaction in the mempool, since we've @@ -817,23 +829,6 @@ func testErrorHandlingOnChainFailure(ht *lntest.HarnessTest) { lnrpc.Failure_INVALID_ONION_BLINDING, ) - // Clean up the rest of our force close: mine blocks so that Bob's CSV - // expires to trigger his sweep and then mine it. - ht.MineBlocks(node.DefaultCSV) - ht.AssertNumPendingSweeps(bob, 1) - ht.MineBlocksAndAssertNumTxes(1, 1) - - // Bring carol back up so that we can close out the rest of our - // channels cooperatively. She requires an interceptor to start up - // so we just re-register our interceptor. - require.NoError(ht, restartCarol()) - _, err = testCase.carol.RPC.Router.HtlcInterceptor(ctx) - require.NoError(ht, err, "interceptor") - - // Assert that Carol has started up and reconnected to dave so that - // we can close out channels cooperatively. - ht.EnsureConnected(testCase.carol, testCase.dave) - // Manually close out the rest of our channels and cancel (don't use // built in cleanup which will try close the already-force-closed // channel). diff --git a/itest/lnd_wipe_fwdpkgs_test.go b/itest/lnd_wipe_fwdpkgs_test.go index bd567f7a00..9aafd960a0 100644 --- a/itest/lnd_wipe_fwdpkgs_test.go +++ b/itest/lnd_wipe_fwdpkgs_test.go @@ -106,7 +106,4 @@ func testWipeForwardingPackages(ht *lntest.HarnessTest) { // Mine 1 block to get Alice's sweeping tx confirmed. ht.MineBlocksAndAssertNumTxes(1, 1) - - // Clean up the force closed channel. - ht.CleanupForceClose(bob) } From 3535cca016d9db53dc7c4b7ac84b131b8fd463e1 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 7 Nov 2024 23:19:19 +0800 Subject: [PATCH 138/153] itest: track and skip flaky tests for windows To make the CI indicative, we now starting tracking the flaky tests found when running on Windows. As a starting point, rather than ignore the windows CI entirely, we now identify there are cases where lnd can be buggy when running in windows. We should fix the tests in the future, otherwise the windows build should be deleted. --- itest/list_exclude_test.go | 108 +++++++++++++++++++++++++++++++++++++ itest/list_on_test.go | 15 ++++++ itest/lnd_test.go | 5 ++ 3 files changed, 128 insertions(+) create mode 100644 itest/list_exclude_test.go diff --git a/itest/list_exclude_test.go b/itest/list_exclude_test.go new file mode 100644 index 0000000000..d6b20cbbea --- /dev/null +++ b/itest/list_exclude_test.go @@ -0,0 +1,108 @@ +//go:build integration + +package itest + +import ( + "fmt" + + "github.com/lightningnetwork/lnd/fn" + "github.com/lightningnetwork/lnd/lntest" +) + +// excludedTestsWindows is a list of tests that are flaky on Windows and should +// be excluded from the test suite atm. +// +// TODO(yy): fix these tests and remove them from this list. +var excludedTestsWindows = []string{ + "batch channel funding", + "zero conf channel open", + "open channel with unstable utxos", + "funding flow persistence", + "channel policy update public zero conf", + + "listsweeps", + "sweep htlcs", + "sweep cpfp anchor incoming timeout", + "payment succeeded htlc remote swept", + "3rd party anchor spend", + + "send payment amp", + "async payments benchmark", + "async bidirectional payments", + + "multihop htlc aggregation leased", + "multihop htlc aggregation leased zero conf", + "multihop htlc aggregation anchor", + "multihop htlc aggregation anchor zero conf", + "multihop htlc aggregation simple taproot", + "multihop htlc aggregation simple taproot zero conf", + + "channel force closure anchor", + "channel force closure simple taproot", + "channel backup restore force close", + "wipe forwarding packages", + + "coop close with htlcs", + "coop close with external delivery", + + "forward interceptor restart", + "forward interceptor dedup htlcs", + "invoice HTLC modifier basic", + "lookup htlc resolution", + + "remote signer taproot", + "remote signer account import", + "remote signer bump fee", + "remote signer funding input types", + "remote signer funding async payments taproot", + "remote signer funding async payments", + "remote signer random seed", + "remote signer verify msg", + "remote signer channel open", + "remote signer shared key", + "remote signer psbt", + "remote signer sign output raw", + + "on chain to blinded", + "query blinded route", + + "data loss protection", +} + +// filterWindowsFlakyTests filters out the flaky tests that are excluded from +// the test suite on Windows. +func filterWindowsFlakyTests() []*lntest.TestCase { + // filteredTestCases is a substest of allTestCases that excludes the + // above flaky tests. + filteredTestCases := make([]*lntest.TestCase, 0, len(allTestCases)) + + // Create a set for the excluded test cases for fast lookup. + excludedSet := fn.NewSet(excludedTestsWindows...) + + // Remove the tests from the excludedSet if it's found in the list of + // all test cases. This is done to ensure the excluded tests are not + // pointing to a test case that doesn't exist. + for _, tc := range allTestCases { + if excludedSet.Contains(tc.Name) { + excludedSet.Remove(tc.Name) + + continue + } + + filteredTestCases = append(filteredTestCases, tc) + } + + // Exit early if all the excluded tests are found in allTestCases. + if excludedSet.IsEmpty() { + return filteredTestCases + } + + // Otherwise, print out the tests that are not found in allTestCases. + errStr := "\nThe following tests are not found, please make sure the " + + "test names are correct in `excludedTestsWindows`.\n" + for _, name := range excludedSet.ToSlice() { + errStr += fmt.Sprintf("Test not found in test suite: %v", name) + } + + panic(errStr) +} diff --git a/itest/list_on_test.go b/itest/list_on_test.go index cdc976fe1b..94978e04e0 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -683,4 +683,19 @@ func init() { allTestCases = append(allTestCases, walletImportAccountTestCases...) allTestCases = append(allTestCases, basicFundingTestCases...) allTestCases = append(allTestCases, sendToRouteTestCases...) + + // Prepare the test cases for windows to exclude some of the flaky + // ones. + // + // NOTE: We need to run this before the isWindowsOS check to make sure + // the excluded tests are found in allTestCases. Otherwise, if a + // non-existing test is included in excludedTestsWindows, we won't be + // able to find it until it's pushed to the CI, which creates a much + // longer feedback loop. + windowsTestCases := filterWindowsFlakyTests() + + // If this is Windows, we'll skip running some of the flaky tests. + if isWindowsOS() { + allTestCases = windowsTestCases + } } diff --git a/itest/lnd_test.go b/itest/lnd_test.go index ebc751ab28..2cc2fa0e19 100644 --- a/itest/lnd_test.go +++ b/itest/lnd_test.go @@ -233,6 +233,11 @@ func getLndBinary(t *testing.T) string { return binary } +// isWindowsOS returns true if the test is running on a Windows OS. +func isWindowsOS() bool { + return runtime.GOOS == "windows" +} + func init() { // Before we start any node, we need to make sure that any btcd node // that is started through the RPC harness uses a unique port as well From f2d5b366fd9b39d7ecc54daeef870f5233f4f897 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 9 Nov 2024 22:39:34 +0800 Subject: [PATCH 139/153] lntest: increase node start timeout and payment benchmark timeout --- lntest/wait/timeouts_darwin.go | 16 +++++++++++++++- lntest/wait/timeouts_remote_db.go | 2 +- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/lntest/wait/timeouts_darwin.go b/lntest/wait/timeouts_darwin.go index f992d06b22..42b1d0dc6c 100644 --- a/lntest/wait/timeouts_darwin.go +++ b/lntest/wait/timeouts_darwin.go @@ -29,7 +29,21 @@ const ( // NodeStartTimeout is the timeout value when waiting for a node to // become fully started. - NodeStartTimeout = time.Minute * 2 + // + // TODO(yy): There is an optimization we can do to increase the time it + // takes to finish the initial wallet sync. Instead of finding the + // block birthday using binary search in btcwallet, we can instead + // search optimistically by looking at the chain tip minus X blocks to + // get the birthday block. This way in the test the node won't attempt + // to sync from the beginning of the chain, which is always the case + // due to how regtest blocks are mined. + // The other direction of optimization is to change the precision of + // the regtest block's median time. By consensus, we need to increase + // at least one second(?), this means in regtest when large amount of + // blocks are mined in a short time, the block time is actually in the + // future. We could instead allow the median time to increase by + // microseconds for itests. + NodeStartTimeout = time.Minute * 3 // SqliteBusyTimeout is the maximum time that a call to the sqlite db // will wait for the connection to become available. diff --git a/lntest/wait/timeouts_remote_db.go b/lntest/wait/timeouts_remote_db.go index 43cd6e022b..ae7043ff0e 100644 --- a/lntest/wait/timeouts_remote_db.go +++ b/lntest/wait/timeouts_remote_db.go @@ -29,7 +29,7 @@ const ( // AsyncBenchmarkTimeout is the timeout used when running the async // payments benchmark. - AsyncBenchmarkTimeout = time.Minute*2 + extraTimeout + AsyncBenchmarkTimeout = time.Minute*5 + extraTimeout // NodeStartTimeout is the timeout value when waiting for a node to // become fully started. From 41f23874b05a31b3e667a9813244eb72c0ca4280 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 21 Nov 2024 22:31:13 +0800 Subject: [PATCH 140/153] lntest: make sure policies are populated in `AssertChannelInGraph` --- lntest/harness_assertion.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lntest/harness_assertion.go b/lntest/harness_assertion.go index 0c14cd9e08..5cc6edeeda 100644 --- a/lntest/harness_assertion.go +++ b/lntest/harness_assertion.go @@ -1859,6 +1859,18 @@ func (h *HarnessTest) AssertChannelInGraph(hn *node.HarnessNode, op, err) } + // Make sure the policies are populated, otherwise this edge + // cannot be used for routing. + if resp.Node1Policy == nil { + return fmt.Errorf("channel %s has no policy1: %w", + op, err) + } + + if resp.Node2Policy == nil { + return fmt.Errorf("channel %s has no policy2: %w", + op, err) + } + edge = resp return nil From b011d8c1de39bd8ef119c95a3a943bd9ee5f4642 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sun, 10 Nov 2024 00:36:08 +0800 Subject: [PATCH 141/153] workflows: use `btcd` for macOS --- .github/workflows/main.yml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 38de5d78be..9cbfff845a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -375,14 +375,8 @@ jobs: go-version: '${{ env.GO_VERSION }}' key-prefix: integration-test - - name: install bitcoind - run: | - wget https://bitcoincore.org/bin/bitcoin-core-${BITCOIN_VERSION}.0/bitcoin-${BITCOIN_VERSION}.0-arm64-apple-darwin.tar.gz - tar zxvf bitcoin-${BITCOIN_VERSION}.0-arm64-apple-darwin.tar.gz - mv bitcoin-${BITCOIN_VERSION}.0 /tmp/bitcoin - - name: run itest - run: PATH=$PATH:/tmp/bitcoin/bin make itest-parallel tranches=${{ env.TRANCHES }} backend=bitcoind shuffleseed=${{ github.run_id }} + run: PATH=$PATH:/tmp/bitcoin/bin make itest-parallel tranches=${{ env.TRANCHES }} shuffleseed=${{ github.run_id }} - name: Zip log files on failure if: ${{ failure() }} From be5535517bcd9bd490005ebb08cba616d4e8b281 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Mon, 25 Nov 2024 21:56:47 +0800 Subject: [PATCH 142/153] itest+lntest: add new method `FundNumCoins` Most of the time we only need to fund the node with given number of UTXOs without concerning the amount, so we add the more efficient funding method as it mines a single block in the end. --- itest/lnd_sweep_test.go | 12 ++++++------ lntest/harness.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/itest/lnd_sweep_test.go b/itest/lnd_sweep_test.go index 5d76557972..e7710f3c2a 100644 --- a/itest/lnd_sweep_test.go +++ b/itest/lnd_sweep_test.go @@ -771,24 +771,24 @@ func testSweepHTLCs(ht *lntest.HarnessTest) { // Bob needs two more wallet utxos: // - when sweeping anchors, he needs one utxo for each sweep. // - when sweeping HTLCs, he needs one utxo for each sweep. - ht.FundCoins(btcutil.SatoshiPerBitcoin, bob) - ht.FundCoins(btcutil.SatoshiPerBitcoin, bob) + numUTXOs := 2 // 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) + // TODO(yy): remove this extra UTXO once the issue is resolved. + numUTXOs++ // For neutrino backend, we need two more UTXOs for Bob to create his // sweeping txns. if ht.IsNeutrinoBackend() { - ht.FundCoins(btcutil.SatoshiPerBitcoin, bob) - ht.FundCoins(btcutil.SatoshiPerBitcoin, bob) + numUTXOs += 2 } + ht.FundNumCoins(bob, numUTXOs) + // Subscribe the invoices. stream1 := carol.RPC.SubscribeSingleInvoice(payHashSettled[:]) stream2 := carol.RPC.SubscribeSingleInvoice(payHashHold[:]) diff --git a/lntest/harness.go b/lntest/harness.go index 82adbd9891..8e9e6310ce 100644 --- a/lntest/harness.go +++ b/lntest/harness.go @@ -1379,6 +1379,40 @@ func (h *HarnessTest) FundCoinsP2TR(amt btcutil.Amount, h.fundCoins(amt, target, lnrpc.AddressType_TAPROOT_PUBKEY, true) } +// FundNumCoins attempts to send the given number of UTXOs from the internal +// mining node to the targeted lightning node using a P2WKH address. Each UTXO +// has an amount of 1 BTC. 1 blocks are mined to confirm the tx. +func (h *HarnessTest) FundNumCoins(hn *node.HarnessNode, num int) { + // Get the initial balance first. + resp := hn.RPC.WalletBalance() + initialBalance := btcutil.Amount(resp.ConfirmedBalance) + + const fundAmount = 1 * btcutil.SatoshiPerBitcoin + + // Send out the outputs from the miner. + for i := 0; i < num; i++ { + h.createAndSendOutput( + hn, fundAmount, lnrpc.AddressType_WITNESS_PUBKEY_HASH, + ) + } + + // Wait for ListUnspent to show the correct number of unconfirmed + // UTXOs. + // + // Since neutrino doesn't support unconfirmed outputs, skip this check. + if !h.IsNeutrinoBackend() { + h.AssertNumUTXOsUnconfirmed(hn, num) + } + + // Mine a block to confirm the transactions. + h.MineBlocksAndAssertNumTxes(1, num) + + // Now block until the wallet have fully synced up. + totalAmount := btcutil.Amount(fundAmount * num) + expectedBalance := initialBalance + totalAmount + h.WaitForBalanceConfirmed(hn, expectedBalance) +} + // completePaymentRequestsAssertStatus sends payments from a node to complete // all payment requests. This function does not return until all payments // have reached the specified status. From 87ea4f0292c31414c5afe22fa7699f5063ccdee5 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Mon, 25 Nov 2024 17:15:34 +0800 Subject: [PATCH 143/153] lntest: limit the num of blocks mined in each test --- lntest/harness.go | 44 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/lntest/harness.go b/lntest/harness.go index 8e9e6310ce..81eda061be 100644 --- a/lntest/harness.go +++ b/lntest/harness.go @@ -326,11 +326,8 @@ func (h *HarnessTest) Subtest(t *testing.T) *HarnessTest { startHeight := int32(h.CurrentHeight()) st.Cleanup(func() { - _, endHeight := h.GetBestBlock() - - st.Logf("finished test: %s, start height=%d, end height=%d, "+ - "mined blocks=%d", st.manager.currentTestCase, - startHeight, endHeight, endHeight-startHeight) + // Make sure the test is not consuming too many blocks. + st.checkAndLimitBlocksMined(startHeight) // Don't bother run the cleanups if the test is failed. if st.Failed() { @@ -368,6 +365,43 @@ func (h *HarnessTest) Subtest(t *testing.T) *HarnessTest { return st } +// checkAndLimitBlocksMined asserts that the blocks mined in a single test +// doesn't exceed 50, which implicitly discourage table-drive tests, which are +// hard to maintain and take a long time to run. +func (h *HarnessTest) checkAndLimitBlocksMined(startHeight int32) { + _, endHeight := h.GetBestBlock() + blocksMined := endHeight - startHeight + + h.Logf("finished test: %s, start height=%d, end height=%d, mined "+ + "blocks=%d", h.manager.currentTestCase, startHeight, endHeight, + blocksMined) + + // If the number of blocks is less than 40, we consider the test + // healthy. + if blocksMined < 40 { + return + } + + // Otherwise log a warning if it's mining more than 40 blocks. + desc := "!============================================!\n" + + desc += fmt.Sprintf("Too many blocks (%v) mined in one test! Tips:\n", + blocksMined) + + desc += "1. break test into smaller individual tests, especially if " + + "this is a table-drive test.\n" + + "2. use smaller CSV via `--bitcoin.defaultremotedelay=1.`\n" + + "3. use smaller CLTV via `--bitcoin.timelockdelta=18.`\n" + + "4. remove unnecessary CloseChannel when test ends.\n" + + "5. use `CreateSimpleNetwork` for efficient channel creation.\n" + h.Log(desc) + + // We enforce that the test should not mine more than 50 blocks, which + // is more than enough to test a multi hop force close scenario. + require.LessOrEqual(h, int(blocksMined), 50, "cannot mine more than "+ + "50 blocks in one test") +} + // shutdownAllNodes will shutdown all running nodes. func (h *HarnessTest) shutdownAllNodes() { for _, node := range h.manager.activeNodes { From 7ed3548a1e43020fdb94151170032f26a9485eb5 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 26 Nov 2024 17:45:12 +0800 Subject: [PATCH 144/153] docs: update release notes --- docs/release-notes/release-notes-0.19.0.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/release-notes-0.19.0.md b/docs/release-notes/release-notes-0.19.0.md index e9d64fc677..fbb2630452 100644 --- a/docs/release-notes/release-notes-0.19.0.md +++ b/docs/release-notes/release-notes-0.19.0.md @@ -189,6 +189,11 @@ The underlying functionality between those two options remain the same. for the Gossip 1.75 protocol. ## Testing + +* The integration tests CI have been optimized to run faster and all flakes are + now documented and + [fixedo](https://github.com/lightningnetwork/lnd/pull/9260). + ## Database * [Migrate the mission control @@ -200,8 +205,6 @@ The underlying functionality between those two options remain the same. store](https://github.com/lightningnetwork/lnd/pull/9001) so that results are namespaced. All existing results are written to the "default" namespace. -## Code Health - ## Tooling and Documentation * [Improved `lncli create` command help text](https://github.com/lightningnetwork/lnd/pull/9077) From 7a07c37363290e217e2b0fbd59ce928675f300fb Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Wed, 30 Oct 2024 14:40:23 -0700 Subject: [PATCH 145/153] Reapply "kvdb/postgres: remove global application level lock" This reverts commit 67419a7c0c6410761ee369c1d24aba8641b8e400. --- kvdb/postgres/db.go | 1 - kvdb/sqlbase/db.go | 8 ------- kvdb/sqlbase/readwrite_tx.go | 44 ------------------------------------ 3 files changed, 53 deletions(-) diff --git a/kvdb/postgres/db.go b/kvdb/postgres/db.go index 90ca8324a8..425ba16225 100644 --- a/kvdb/postgres/db.go +++ b/kvdb/postgres/db.go @@ -28,7 +28,6 @@ func newPostgresBackend(ctx context.Context, config *Config, prefix string) ( Schema: "public", TableNamePrefix: prefix, SQLiteCmdReplacements: sqliteCmdReplacements, - WithTxLevelLock: true, } return sqlbase.NewSqlBackend(ctx, cfg) diff --git a/kvdb/sqlbase/db.go b/kvdb/sqlbase/db.go index 221a77bfd6..6ef085712a 100644 --- a/kvdb/sqlbase/db.go +++ b/kvdb/sqlbase/db.go @@ -55,10 +55,6 @@ type Config struct { // commands. Note that the sqlite keywords to be replaced are // case-sensitive. SQLiteCmdReplacements SQLiteCmdReplacements - - // WithTxLevelLock when set will ensure that there is a transaction - // level lock. - WithTxLevelLock bool } // db holds a reference to the sql db connection. @@ -79,10 +75,6 @@ type db struct { // db is the underlying database connection instance. db *sql.DB - // lock is the global write lock that ensures single writer. This is - // only used if cfg.WithTxLevelLock is set. - lock sync.RWMutex - // table is the name of the table that contains the data for all // top-level buckets that have keys that cannot be mapped to a distinct // sql table. diff --git a/kvdb/sqlbase/readwrite_tx.go b/kvdb/sqlbase/readwrite_tx.go index ec761931ad..18a6a682c9 100644 --- a/kvdb/sqlbase/readwrite_tx.go +++ b/kvdb/sqlbase/readwrite_tx.go @@ -5,7 +5,6 @@ package sqlbase import ( "context" "database/sql" - "sync" "github.com/btcsuite/btcwallet/walletdb" ) @@ -20,28 +19,11 @@ type readWriteTx struct { // active is true if the transaction hasn't been committed yet. active bool - - // locker is a pointer to the global db lock. - locker sync.Locker } // newReadWriteTx creates an rw transaction using a connection from the // specified pool. func newReadWriteTx(db *db, readOnly bool) (*readWriteTx, error) { - locker := newNoopLocker() - if db.cfg.WithTxLevelLock { - // Obtain the global lock instance. An alternative here is to - // obtain a database lock from Postgres. Unfortunately there is - // no database-level lock in Postgres, meaning that each table - // would need to be locked individually. Perhaps an advisory - // lock could perform this function too. - locker = &db.lock - if readOnly { - locker = db.lock.RLocker() - } - } - locker.Lock() - // Start the transaction. Don't use the timeout context because it would // be applied to the transaction as a whole. If possible, mark the // transaction as read-only to make sure that potential programming @@ -54,7 +36,6 @@ func newReadWriteTx(db *db, readOnly bool) (*readWriteTx, error) { }, ) if err != nil { - locker.Unlock() return nil, err } @@ -62,7 +43,6 @@ func newReadWriteTx(db *db, readOnly bool) (*readWriteTx, error) { db: db, tx: tx, active: true, - locker: locker, }, nil } @@ -94,7 +74,6 @@ func (tx *readWriteTx) Rollback() error { // Unlock the transaction regardless of the error result. tx.active = false - tx.locker.Unlock() return err } @@ -162,7 +141,6 @@ func (tx *readWriteTx) Commit() error { // Unlock the transaction regardless of the error result. tx.active = false - tx.locker.Unlock() return err } @@ -204,25 +182,3 @@ func (tx *readWriteTx) Exec(query string, args ...interface{}) (sql.Result, return tx.tx.ExecContext(ctx, query, args...) } - -// noopLocker is an implementation of a no-op sync.Locker. -type noopLocker struct{} - -// newNoopLocker creates a new noopLocker. -func newNoopLocker() sync.Locker { - return &noopLocker{} -} - -// Lock is a noop. -// -// NOTE: this is part of the sync.Locker interface. -func (n *noopLocker) Lock() { -} - -// Unlock is a noop. -// -// NOTE: this is part of the sync.Locker interface. -func (n *noopLocker) Unlock() { -} - -var _ sync.Locker = (*noopLocker)(nil) From 087b9774fe9af4b77e89e5c58f52d992ac2b75a8 Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Wed, 30 Oct 2024 15:01:59 -0700 Subject: [PATCH 146/153] go.mod: use local kvdb to reapply removal of global postgres lock --- go.mod | 3 +++ go.sum | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 6e2bd9f77d..d129a63b6c 100644 --- a/go.mod +++ b/go.mod @@ -203,6 +203,9 @@ replace github.com/ulikunitz/xz => github.com/ulikunitz/xz v0.5.11 // https://deps.dev/advisory/OSV/GO-2021-0053?from=%2Fgo%2Fgithub.com%252Fgogo%252Fprotobuf%2Fv1.3.1 replace github.com/gogo/protobuf => github.com/gogo/protobuf v1.3.2 +// Use local kvdb package until new version is tagged. +replace github.com/lightningnetwork/lnd/kvdb => ./kvdb + // We want to format raw bytes as hex instead of base64. The forked version // allows us to specify that as an option. replace google.golang.org/protobuf => github.com/lightninglabs/protobuf-go-hex-display v1.30.0-hex-display diff --git a/go.sum b/go.sum index 86c1c8a21a..a5370bb1b1 100644 --- a/go.sum +++ b/go.sum @@ -460,8 +460,6 @@ github.com/lightningnetwork/lnd/fn v1.2.3 h1:Q1OrgNSgQynVheBNa16CsKVov1JI5N2AR6G github.com/lightningnetwork/lnd/fn v1.2.3/go.mod h1:SyFohpVrARPKH3XVAJZlXdVe+IwMYc4OMAvrDY32kw0= github.com/lightningnetwork/lnd/healthcheck v1.2.6 h1:1sWhqr93GdkWy4+6U7JxBfcyZIE78MhIHTJZfPx7qqI= github.com/lightningnetwork/lnd/healthcheck v1.2.6/go.mod h1:Mu02um4CWY/zdTOvFje7WJgJcHyX2zq/FG3MhOAiGaQ= -github.com/lightningnetwork/lnd/kvdb v1.4.11 h1:fk1HMVFrsVK3xqU7q+JWHRgBltw/a2qIg1E3zazMb/8= -github.com/lightningnetwork/lnd/kvdb v1.4.11/go.mod h1:a5cMDKMjbJA8dD06ZqqnYkmSh5DhEbbG8C1YHM3NN+k= github.com/lightningnetwork/lnd/queue v1.1.1 h1:99ovBlpM9B0FRCGYJo6RSFDlt8/vOkQQZznVb18iNMI= github.com/lightningnetwork/lnd/queue v1.1.1/go.mod h1:7A6nC1Qrm32FHuhx/mi1cieAiBZo5O6l8IBIoQxvkz4= github.com/lightningnetwork/lnd/sqldb v1.0.5 h1:ax5vBPf44tN/uD6C5+hBPBjOJ7cRMrUL+sVOdzmLVt4= From a06d29cffe3679e684b538c366831a17fab1edec Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Mon, 18 Nov 2024 15:28:20 -0800 Subject: [PATCH 147/153] itest: fix flake in multi-hop payments To make this itest work reliably with multiple parallel SQL transactions, we need to count both the settle and final HTLC events. Otherwise, sometimes the final events from earlier forwards are counted before the forward events from later forwards, causing a miscount of the settle events. If we expect both the settle and final event for each forward, we don't miscount. --- itest/lnd_multi-hop-payments_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/itest/lnd_multi-hop-payments_test.go b/itest/lnd_multi-hop-payments_test.go index e965140523..9c55d7845a 100644 --- a/itest/lnd_multi-hop-payments_test.go +++ b/itest/lnd_multi-hop-payments_test.go @@ -223,11 +223,11 @@ func testMultiHopPayments(ht *lntest.HarnessTest) { // Dave and Alice should both have forwards and settles for // their role as forwarding nodes. ht.AssertHtlcEvents( - daveEvents, numPayments, 0, numPayments, 0, + daveEvents, numPayments, 0, numPayments*2, 0, routerrpc.HtlcEvent_FORWARD, ) ht.AssertHtlcEvents( - aliceEvents, numPayments, 0, numPayments, 0, + aliceEvents, numPayments, 0, numPayments*2, 0, routerrpc.HtlcEvent_FORWARD, ) From 1b79205c401968952424f36d5de83e226b390e94 Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Fri, 1 Nov 2024 19:50:05 -0700 Subject: [PATCH 148/153] batch: handle serialization errors correctly --- batch/batch.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/batch/batch.go b/batch/batch.go index fcc691582a..750c66f6d7 100644 --- a/batch/batch.go +++ b/batch/batch.go @@ -5,6 +5,7 @@ import ( "sync" "github.com/lightningnetwork/lnd/kvdb" + "github.com/lightningnetwork/lnd/sqldb" ) // errSolo is a sentinel error indicating that the requester should re-run the @@ -55,7 +56,19 @@ func (b *batch) run() { for i, req := range b.reqs { err := req.Update(tx) if err != nil { - failIdx = i + // If we get a serialization error, we + // want the underlying SQL retry + // mechanism to retry the entire batch. + // Otherwise, we can succeed in an + // sqldb retry and still re-execute the + // failing request individually. + dbErr := sqldb.MapSQLError(err) + if !sqldb.IsSerializationError(dbErr) { + failIdx = i + + return dbErr + } + return err } } From 41ccd4c0988ad6677ae29376773d9cac0c58ab6f Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Mon, 4 Nov 2024 19:15:35 -0800 Subject: [PATCH 149/153] channeldb: handle previously-unhandled errors --- channeldb/graph.go | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/channeldb/graph.go b/channeldb/graph.go index d7a4480d03..f9cd9ac271 100644 --- a/channeldb/graph.go +++ b/channeldb/graph.go @@ -1083,7 +1083,7 @@ func (c *ChannelGraph) addChannelEdge(tx kvdb.RwTx, err := addLightningNode(tx, &node1Shell) if err != nil { return fmt.Errorf("unable to create shell node "+ - "for: %x", edge.NodeKey1Bytes) + "for: %x: %w", edge.NodeKey1Bytes, err) } case node1Err != nil: return err @@ -1099,7 +1099,7 @@ func (c *ChannelGraph) addChannelEdge(tx kvdb.RwTx, err := addLightningNode(tx, &node2Shell) if err != nil { return fmt.Errorf("unable to create shell node "+ - "for: %x", edge.NodeKey2Bytes) + "for: %x: %w", edge.NodeKey2Bytes, err) } case node2Err != nil: return err @@ -2626,8 +2626,14 @@ func (c *ChannelGraph) delChannelEdgeUnsafe(edges, edgeIndex, chanIndex, // As part of deleting the edge we also remove all disabled entries // from the edgePolicyDisabledIndex bucket. We do that for both directions. - updateEdgePolicyDisabledIndex(edges, cid, false, false) - updateEdgePolicyDisabledIndex(edges, cid, true, false) + err = updateEdgePolicyDisabledIndex(edges, cid, false, false) + if err != nil { + return err + } + err = updateEdgePolicyDisabledIndex(edges, cid, true, false) + if err != nil { + return err + } // With the edge data deleted, we can purge the information from the two // edge indexes. @@ -4456,11 +4462,14 @@ func putChanEdgePolicy(edges kvdb.RwBucket, edge *models.ChannelEdgePolicy, return err } - updateEdgePolicyDisabledIndex( + err = updateEdgePolicyDisabledIndex( edges, edge.ChannelID, edge.ChannelFlags&lnwire.ChanUpdateDirection > 0, edge.IsDisabled(), ) + if err != nil { + return err + } return edges.Put(edgeKey[:], b.Bytes()[:]) } From 32edc7432c5f7f28be2cbcb0ca7a8003b52965ee Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Wed, 6 Nov 2024 11:29:47 -0800 Subject: [PATCH 150/153] sqldb: improve serialization error handling --- go.mod | 3 +++ go.sum | 2 -- kvdb/go.mod | 6 +++++- kvdb/go.sum | 14 ++++++-------- sqldb/interfaces.go | 2 +- sqldb/sqlerrors.go | 32 ++++++++++++++++++++++++++++---- 6 files changed, 43 insertions(+), 16 deletions(-) diff --git a/go.mod b/go.mod index d129a63b6c..b7dd6a9d15 100644 --- a/go.mod +++ b/go.mod @@ -206,6 +206,9 @@ replace github.com/gogo/protobuf => github.com/gogo/protobuf v1.3.2 // Use local kvdb package until new version is tagged. replace github.com/lightningnetwork/lnd/kvdb => ./kvdb +// Use local sqldb package until new version is tagged. +replace github.com/lightningnetwork/lnd/sqldb => ./sqldb + // We want to format raw bytes as hex instead of base64. The forked version // allows us to specify that as an option. replace google.golang.org/protobuf => github.com/lightninglabs/protobuf-go-hex-display v1.30.0-hex-display diff --git a/go.sum b/go.sum index a5370bb1b1..05f8bcb349 100644 --- a/go.sum +++ b/go.sum @@ -462,8 +462,6 @@ github.com/lightningnetwork/lnd/healthcheck v1.2.6 h1:1sWhqr93GdkWy4+6U7JxBfcyZI github.com/lightningnetwork/lnd/healthcheck v1.2.6/go.mod h1:Mu02um4CWY/zdTOvFje7WJgJcHyX2zq/FG3MhOAiGaQ= github.com/lightningnetwork/lnd/queue v1.1.1 h1:99ovBlpM9B0FRCGYJo6RSFDlt8/vOkQQZznVb18iNMI= github.com/lightningnetwork/lnd/queue v1.1.1/go.mod h1:7A6nC1Qrm32FHuhx/mi1cieAiBZo5O6l8IBIoQxvkz4= -github.com/lightningnetwork/lnd/sqldb v1.0.5 h1:ax5vBPf44tN/uD6C5+hBPBjOJ7cRMrUL+sVOdzmLVt4= -github.com/lightningnetwork/lnd/sqldb v1.0.5/go.mod h1:OG09zL/PHPaBJefp4HsPz2YLUJ+zIQHbpgCtLnOx8I4= github.com/lightningnetwork/lnd/ticker v1.1.1 h1:J/b6N2hibFtC7JLV77ULQp++QLtCwT6ijJlbdiZFbSM= github.com/lightningnetwork/lnd/ticker v1.1.1/go.mod h1:waPTRAAcwtu7Ji3+3k+u/xH5GHovTsCoSVpho0KDvdA= github.com/lightningnetwork/lnd/tlv v1.2.6 h1:icvQG2yDr6k3ZuZzfRdG3EJp6pHurcuh3R6dg0gv/Mw= diff --git a/kvdb/go.mod b/kvdb/go.mod index 3dc4281642..f1faf2a293 100644 --- a/kvdb/go.mod +++ b/kvdb/go.mod @@ -16,7 +16,7 @@ require ( go.etcd.io/etcd/client/v3 v3.5.7 go.etcd.io/etcd/server/v3 v3.5.7 golang.org/x/net v0.22.0 - modernc.org/sqlite v1.29.8 + modernc.org/sqlite v1.29.10 ) require ( @@ -59,6 +59,7 @@ require ( github.com/jackc/pgproto3/v2 v2.3.3 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgtype v1.14.0 // indirect + github.com/jackc/pgx/v5 v5.3.1 // indirect github.com/jonboulle/clockwork v0.2.2 // indirect github.com/json-iterator/go v1.1.11 // indirect github.com/lib/pq v1.10.9 // indirect @@ -135,6 +136,9 @@ replace github.com/dgrijalva/jwt-go => github.com/golang-jwt/jwt v3.2.1+incompat // This replace is for https://github.com/advisories/GHSA-25xm-hr59-7c27 replace github.com/ulikunitz/xz => github.com/ulikunitz/xz v0.5.11 +// Use local sqldb package until new version is tagged. +replace github.com/lightningnetwork/lnd/sqldb => ../sqldb + // This replace is for // https://deps.dev/advisory/OSV/GO-2021-0053?from=%2Fgo%2Fgithub.com%252Fgogo%252Fprotobuf%2Fv1.3.1 replace github.com/gogo/protobuf => github.com/gogo/protobuf v1.3.2 diff --git a/kvdb/go.sum b/kvdb/go.sum index 329dd2497c..ff51e77dd9 100644 --- a/kvdb/go.sum +++ b/kvdb/go.sum @@ -257,6 +257,8 @@ github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQ github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= github.com/jackc/pgx/v4 v4.18.1 h1:YP7G1KABtKpB5IHrO9vYwSrCOhs7p3uqhvhhQBptya0= github.com/jackc/pgx/v4 v4.18.1/go.mod h1:FydWkUyadDmdNH/mHnGob881GawxeEm7TcMCzkb+qQE= +github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU= +github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= @@ -297,8 +299,6 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lightningnetwork/lnd/healthcheck v1.2.4 h1:lLPLac+p/TllByxGSlkCwkJlkddqMP5UCoawCj3mgFQ= github.com/lightningnetwork/lnd/healthcheck v1.2.4/go.mod h1:G7Tst2tVvWo7cx6mSBEToQC5L1XOGxzZTPB29g9Rv2I= -github.com/lightningnetwork/lnd/sqldb v1.0.2 h1:PfuYzScYMD9/QonKo/QvgsbXfTnH5DfldIimkfdW4Bk= -github.com/lightningnetwork/lnd/sqldb v1.0.2/go.mod h1:V2Xl6JNWLTKE97WJnwfs0d0TYJdIQTqK8/3aAwkd3qI= github.com/lightningnetwork/lnd/ticker v1.1.0 h1:ShoBiRP3pIxZHaETndfQ5kEe+S4NdAY1hiX7YbZ4QE4= github.com/lightningnetwork/lnd/ticker v1.1.0/go.mod h1:ubqbSVCn6RlE0LazXuBr7/Zi6QT0uQo++OgIRBxQUrk= github.com/lightningnetwork/lnd/tor v1.0.0 h1:wvEc7I+Y7IOtPglVP3cVBbYhiVhc7uTd7cMF9gQRzwA= @@ -310,8 +310,6 @@ github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= -github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg= @@ -383,8 +381,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= @@ -738,8 +736,8 @@ modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= -modernc.org/sqlite v1.29.8 h1:nGKglNx9K5v0As+zF0/Gcl1kMkmaU1XynYyq92PbsC8= -modernc.org/sqlite v1.29.8/go.mod h1:lQPm27iqa4UNZpmr4Aor0MH0HkCLbt1huYDfWylLZFk= +modernc.org/sqlite v1.29.10 h1:3u93dz83myFnMilBGCOLbr+HjklS6+5rJLx4q86RDAg= +modernc.org/sqlite v1.29.10/go.mod h1:ItX2a1OVGgNsFh6Dv60JQvGfJfTPHPVpV6DF59akYOA= modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= diff --git a/sqldb/interfaces.go b/sqldb/interfaces.go index f81799c577..3c042aa5a7 100644 --- a/sqldb/interfaces.go +++ b/sqldb/interfaces.go @@ -22,7 +22,7 @@ const ( // DefaultNumTxRetries is the default number of times we'll retry a // transaction if it fails with an error that permits transaction // repetition. - DefaultNumTxRetries = 10 + DefaultNumTxRetries = 20 // DefaultRetryDelay is the default delay between retries. This will be // used to generate a random delay between 0 and this value. diff --git a/sqldb/sqlerrors.go b/sqldb/sqlerrors.go index 6cc1b1cf87..5fdff83054 100644 --- a/sqldb/sqlerrors.go +++ b/sqldb/sqlerrors.go @@ -17,6 +17,15 @@ var ( // ErrRetriesExceeded is returned when a transaction is retried more // than the max allowed valued without a success. ErrRetriesExceeded = errors.New("db tx retries exceeded") + + // postgresErrMsgs are strings that signify retriable errors resulting + // from serialization failures. + postgresErrMsgs = []string{ + "could not serialize access", + "current transaction is aborted", + "not enough elements in RWConflictPool", + "deadlock detected", + } ) // MapSQLError attempts to interpret a given error as a database agnostic SQL @@ -41,10 +50,11 @@ func MapSQLError(err error) error { // Sometimes the error won't be properly wrapped, so we'll need to // inspect raw error itself to detect something we can wrap properly. // This handles a postgres variant of the error. - const postgresErrMsg = "could not serialize access" - if strings.Contains(err.Error(), postgresErrMsg) { - return &ErrSerializationError{ - DBError: err, + for _, postgresErrMsg := range postgresErrMsgs { + if strings.Contains(err.Error(), postgresErrMsg) { + return &ErrSerializationError{ + DBError: err, + } } } @@ -105,6 +115,20 @@ func parsePostgresError(pqErr *pgconn.PgError) error { DBError: pqErr, } + // In failed SQL transaction because we didn't catch a previous + // serialization error, so return this one as a serialization error. + case pgerrcode.InFailedSQLTransaction: + return &ErrSerializationError{ + DBError: pqErr, + } + + // Deadlock detedted because of a serialization error, so return this + // one as a serialization error. + case pgerrcode.DeadlockDetected: + return &ErrSerializationError{ + DBError: pqErr, + } + default: return fmt.Errorf("unknown postgres error: %w", pqErr) } From 837e092fcf2109a3bfe32b05a73caee74aacb55d Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Wed, 6 Nov 2024 10:21:34 -0800 Subject: [PATCH 151/153] Makefile: tune params for db-instance for postgres itests --- Makefile | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 101b9aed34..d89f13c7a1 100644 --- a/Makefile +++ b/Makefile @@ -195,9 +195,11 @@ ifeq ($(dbbackend),postgres) docker rm lnd-postgres --force || echo "Starting new postgres container" # Start a fresh postgres instance. Allow a maximum of 500 connections so - # that multiple lnd instances with a maximum number of connections of 50 - # each can run concurrently. - docker run --name lnd-postgres -e POSTGRES_PASSWORD=postgres -p 6432:5432 -d postgres:13-alpine -N 500 + # that multiple lnd instances with a maximum number of connections of 20 + # each can run concurrently. Note that many of the settings here are + # specifically for integration testing and are not fit for running + # production nodes. + docker run --name lnd-postgres -e POSTGRES_PASSWORD=postgres -p 6432:5432 -d postgres:13-alpine -N 1500 -c max_pred_locks_per_transaction=1024 -c max_locks_per_transaction=128 -c jit=off -c work_mem=8MB -c checkpoint_timeout=10min -c enable_seqscan=off docker logs -f lnd-postgres & # Wait for the instance to be started. From 0ce43847319347eb854d04127f4b42e83661ba87 Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Thu, 14 Nov 2024 21:48:14 -0800 Subject: [PATCH 152/153] Makefile: log to file instead of console --- Makefile | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index d89f13c7a1..7467b6000e 100644 --- a/Makefile +++ b/Makefile @@ -200,16 +200,19 @@ ifeq ($(dbbackend),postgres) # specifically for integration testing and are not fit for running # production nodes. docker run --name lnd-postgres -e POSTGRES_PASSWORD=postgres -p 6432:5432 -d postgres:13-alpine -N 1500 -c max_pred_locks_per_transaction=1024 -c max_locks_per_transaction=128 -c jit=off -c work_mem=8MB -c checkpoint_timeout=10min -c enable_seqscan=off - docker logs -f lnd-postgres & + docker logs -f lnd-postgres >itest/postgres.log 2>&1 & # Wait for the instance to be started. sleep $(POSTGRES_START_DELAY) endif +clean-itest-logs: + rm -rf itest/*.log itest/.logs-* + #? itest-only: Only run integration tests without re-building binaries -itest-only: db-instance +itest-only: clean-itest-logs db-instance @$(call print, "Running integration tests with ${backend} backend.") - rm -rf itest/*.log itest/.logs-*; date + date EXEC_SUFFIX=$(EXEC_SUFFIX) scripts/itest_part.sh 0 1 $(SHUFFLE_SEED) $(TEST_FLAGS) $(ITEST_FLAGS) -test.v $(COLLECT_ITEST_COVERAGE) @@ -220,9 +223,9 @@ itest: build-itest itest-only itest-race: build-itest-race itest-only #? itest-parallel: Build and run integration tests in parallel mode, running up to ITEST_PARALLELISM test tranches in parallel (default 4) -itest-parallel: build-itest db-instance +itest-parallel: clean-itest-logs build-itest db-instance @$(call print, "Running tests") - rm -rf itest/*.log itest/.logs-*; date + date EXEC_SUFFIX=$(EXEC_SUFFIX) scripts/itest_parallel.sh $(ITEST_PARALLELISM) $(NUM_ITEST_TRANCHES) $(SHUFFLE_SEED) $(TEST_FLAGS) $(ITEST_FLAGS) $(COLLECT_ITEST_COVERAGE) From 0fbf7b5438a402288ad8336405bf832b190314b7 Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Fri, 15 Nov 2024 01:41:22 -0800 Subject: [PATCH 153/153] github workflow: save postgres log to zip file --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9cbfff845a..4c9ee5e2e2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -297,7 +297,7 @@ jobs: - name: Zip log files on failure if: ${{ failure() }} timeout-minutes: 5 # timeout after 5 minute - run: 7z a logs-itest-${{ matrix.name }}.zip itest/**/*.log + run: 7z a logs-itest-${{ matrix.name }}.zip itest/**/*.log itest/postgres.log - name: Upload log files on failure uses: actions/upload-artifact@v3