From 9d79fea9375a682171e926463d752e51dc43f444 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Thu, 31 Oct 2019 12:59:15 +0100 Subject: [PATCH 1/8] cnct: fix incoming resolver assertion --- contractcourt/htlc_incoming_resolver_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contractcourt/htlc_incoming_resolver_test.go b/contractcourt/htlc_incoming_resolver_test.go index ab8ad6ec..850126c0 100644 --- a/contractcourt/htlc_incoming_resolver_test.go +++ b/contractcourt/htlc_incoming_resolver_test.go @@ -294,7 +294,7 @@ func (i *incomingResolverTestContext) waitForResult(expectSuccessRes bool) { } if !expectSuccessRes { - if err != nil { + if i.nextResolver != nil { i.t.Fatal("expected no next resolver") } return From 55a32c951aecb12b85f4df550f21e5887094ddd1 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Wed, 30 Oct 2019 11:32:27 +0100 Subject: [PATCH 2/8] cnct: prefix logger for commit sweep resolver Unify resolver specific log statements. Leaves modification of the other resolvers for a later moment when it can be combined with a real change. --- contractcourt/commit_sweep_resolver.go | 38 ++++++++++++++------------ contractcourt/contract_resolvers.go | 11 ++++++++ 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/contractcourt/commit_sweep_resolver.go b/contractcourt/commit_sweep_resolver.go index 3b7f3b85..874b1567 100644 --- a/contractcourt/commit_sweep_resolver.go +++ b/contractcourt/commit_sweep_resolver.go @@ -44,12 +44,16 @@ func newCommitSweepResolver(res lnwallet.CommitOutputResolution, broadcastHeight uint32, chanPoint wire.OutPoint, resCfg ResolverConfig) *commitSweepResolver { - return &commitSweepResolver{ + r := &commitSweepResolver{ contractResolverKit: *newContractResolverKit(resCfg), commitResolution: res, broadcastHeight: broadcastHeight, chanPoint: chanPoint, } + + r.initLogger(r) + + return r } // ResolverKey returns an identifier which should be globally unique for this @@ -85,7 +89,7 @@ func (c *commitSweepResolver) Resolve() (ContractResolver, error) { return nil, err } - log.Debugf("%T(%v): waiting for commit tx to confirm", c, c.chanPoint) + c.log.Debugf("waiting for commit tx to confirm") select { case _, ok := <-confNtfn.Confirmed: @@ -125,13 +129,12 @@ func (c *commitSweepResolver) Resolve() (ContractResolver, error) { // With our input constructed, we'll now offer it to the // sweeper. - log.Infof("%T(%v): sweeping commit output", c, c.chanPoint) + c.log.Infof("sweeping commit output") feePref := sweep.FeePreference{ConfTarget: commitOutputConfTarget} resultChan, err := c.Sweeper.SweepInput(&inp, feePref) if err != nil { - log.Errorf("%T(%v): unable to sweep input: %v", - c, c.chanPoint, err) + c.log.Errorf("unable to sweep input: %v", err) return nil, err } @@ -143,14 +146,14 @@ func (c *commitSweepResolver) Resolve() (ContractResolver, error) { select { case sweepResult := <-resultChan: if sweepResult.Err != nil { - log.Errorf("%T(%v): unable to sweep input: %v", - c, c.chanPoint, sweepResult.Err) + c.log.Errorf("unable to sweep input: %v", + sweepResult.Err) return nil, sweepResult.Err } - log.Infof("ChannelPoint(%v) commit tx is fully resolved by "+ - "sweep tx: %v", c.chanPoint, sweepResult.Tx.TxHash()) + c.log.Infof("commit tx fully resolved by sweep tx: %v", + sweepResult.Tx.TxHash()) case <-c.quit: return nil, errResolverShuttingDown } @@ -171,8 +174,7 @@ func (c *commitSweepResolver) Resolve() (ContractResolver, error) { return nil, err } - log.Infof("%T(%v): waiting for commit output to be swept", c, - c.chanPoint) + c.log.Infof("waiting for commit output to be swept") var sweepTx *wire.MsgTx select { @@ -186,19 +188,17 @@ func (c *commitSweepResolver) Resolve() (ContractResolver, error) { // now consider this to be our sweep transaction. sweepTx = commitSpend.SpendingTx - log.Infof("%T(%v): commit output swept by txid=%v", - c, c.chanPoint, sweepTx.TxHash()) + c.log.Infof("commit output swept by txid=%v", sweepTx.TxHash()) if err := c.Checkpoint(c); err != nil { - log.Errorf("unable to Checkpoint: %v", err) + c.log.Errorf("unable to Checkpoint: %v", err) return nil, err } case <-c.quit: return nil, errResolverShuttingDown } - log.Infof("%T(%v): waiting for commit sweep txid=%v conf", c, c.chanPoint, - sweepTx.TxHash()) + c.log.Infof("waiting for commit sweep txid=%v conf", sweepTx.TxHash()) // Now we'll wait until the sweeping transaction has been fully // confirmed. Once it's confirmed, we can mark this contract resolved. @@ -216,8 +216,8 @@ func (c *commitSweepResolver) Resolve() (ContractResolver, error) { return nil, errResolverShuttingDown } - log.Infof("ChannelPoint(%v) commit tx is fully resolved, at height: %v", - c.chanPoint, confInfo.BlockHeight) + c.log.Infof("commit tx is fully resolved, at height: %v", + confInfo.BlockHeight) case <-c.quit: return nil, errResolverShuttingDown @@ -308,6 +308,8 @@ 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) + return c, nil } diff --git a/contractcourt/contract_resolvers.go b/contractcourt/contract_resolvers.go index 2cda229a..a5fe119a 100644 --- a/contractcourt/contract_resolvers.go +++ b/contractcourt/contract_resolvers.go @@ -3,9 +3,12 @@ package contractcourt import ( "encoding/binary" "errors" + "fmt" "io" "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btclog" + "github.com/lightningnetwork/lnd/build" "github.com/lightningnetwork/lnd/channeldb" ) @@ -93,6 +96,8 @@ type ResolverConfig struct { type contractResolverKit struct { ResolverConfig + log btclog.Logger + quit chan struct{} } @@ -104,6 +109,12 @@ 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) + r.log = build.NewPrefixLog(logPrefix, log) +} + var ( // errResolverShuttingDown is returned when the resolver stops // progressing because it received the quit signal. From 7e472c9e8c3931e8763355cc12f600fef09c44c1 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Tue, 29 Oct 2019 17:17:45 +0100 Subject: [PATCH 3/8] input: add constructor for csv-locked inputs --- input/input.go | 47 +++++++++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/input/input.go b/input/input.go index a63048cb..7f5593ec 100644 --- a/input/input.go +++ b/input/input.go @@ -44,10 +44,11 @@ type Input interface { } type inputKit struct { - outpoint wire.OutPoint - witnessType WitnessType - signDesc SignDescriptor - heightHint uint32 + outpoint wire.OutPoint + witnessType WitnessType + signDesc SignDescriptor + heightHint uint32 + blockToMaturity uint32 } // OutPoint returns the breached output's identifier that is to be included as @@ -74,6 +75,13 @@ func (i *inputKit) HeightHint() uint32 { return i.heightHint } +// BlocksToMaturity returns the relative timelock, as a number of blocks, that +// must be built on top of the confirmation height before the output can be +// spent. For non-CSV locked inputs this is always zero. +func (i *inputKit) BlocksToMaturity() uint32 { + return i.blockToMaturity +} + // BaseInput contains all the information needed to sweep a basic output // (CSV/CLTV/no time lock) type BaseInput struct { @@ -107,6 +115,23 @@ func NewBaseInput(outpoint *wire.OutPoint, witnessType WitnessType, return &input } +// NewCsvInput assembles a new csv-locked input that can be used to +// construct a sweep transaction. +func NewCsvInput(outpoint *wire.OutPoint, witnessType WitnessType, + signDescriptor *SignDescriptor, heightHint uint32, + blockToMaturity uint32) *BaseInput { + + return &BaseInput{ + inputKit{ + outpoint: *outpoint, + witnessType: witnessType, + signDesc: *signDescriptor, + heightHint: heightHint, + blockToMaturity: blockToMaturity, + }, + } +} + // CraftInputScript returns a valid set of input scripts allowing this output // to be spent. The returned input scripts should target the input at location // txIndex within the passed transaction. The input scripts generated by this @@ -119,13 +144,6 @@ func (bi *BaseInput) CraftInputScript(signer Signer, txn *wire.MsgTx, return witnessFunc(txn, hashCache, txinIdx) } -// BlocksToMaturity returns the relative timelock, as a number of blocks, that -// must be built on top of the confirmation height before the output can be -// spent. For non-CSV locked inputs this is always zero. -func (bi *BaseInput) BlocksToMaturity() uint32 { - return 0 -} - // HtlcSucceedInput constitutes a sweep input that needs a pre-image. The input // is expected to reside on the commitment tx of the remote party and should // not be a second level tx output. @@ -175,13 +193,6 @@ func (h *HtlcSucceedInput) CraftInputScript(signer Signer, txn *wire.MsgTx, }, nil } -// BlocksToMaturity returns the relative timelock, as a number of blocks, that -// must be built on top of the confirmation height before the output can be -// spent. -func (h *HtlcSucceedInput) BlocksToMaturity() uint32 { - return 0 -} - // Compile-time constraints to ensure each input struct implement the Input // interface. var _ Input = (*BaseInput)(nil) From 1597a92160f096205a1d93982300573792b43c92 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Wed, 30 Oct 2019 10:31:01 +0100 Subject: [PATCH 4/8] cnct: add resolver report output type This commit prepares for the commit sweep resolver to report on its state. --- contractcourt/channel_arbitrator.go | 18 ++++++++++++++++-- .../htlc_incoming_contest_resolver.go | 2 +- .../htlc_outgoing_contest_resolver.go | 2 +- rpcserver.go | 5 ++++- 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/contractcourt/channel_arbitrator.go b/contractcourt/channel_arbitrator.go index e63a2046..460e6a61 100644 --- a/contractcourt/channel_arbitrator.go +++ b/contractcourt/channel_arbitrator.go @@ -129,13 +129,27 @@ type ChannelArbitratorConfig struct { ChainArbitratorConfig } +// ReportOutputType describes the type of output that is being reported +// on. +type ReportOutputType uint8 + +const ( + // ReportOutputIncomingHtlc is an incoming hash time locked contract on + // the commitment tx. + ReportOutputIncomingHtlc ReportOutputType = iota + + // ReportOutputOutgoingHtlc is an outgoing hash time locked contract on + // the commitment tx. + ReportOutputOutgoingHtlc +) + // ContractReport provides a summary of a commitment tx output. type ContractReport struct { // Outpoint is the final output that will be swept back to the wallet. Outpoint wire.OutPoint - // Incoming indicates whether the htlc was incoming to this channel. - Incoming bool + // Type indicates the type of the reported output. + Type ReportOutputType // Amount is the final value that will be swept in back to the wallet. Amount btcutil.Amount diff --git a/contractcourt/htlc_incoming_contest_resolver.go b/contractcourt/htlc_incoming_contest_resolver.go index 2e089724..abedcc14 100644 --- a/contractcourt/htlc_incoming_contest_resolver.go +++ b/contractcourt/htlc_incoming_contest_resolver.go @@ -293,7 +293,7 @@ func (h *htlcIncomingContestResolver) report() *ContractReport { return &ContractReport{ Outpoint: h.htlcResolution.ClaimOutpoint, - Incoming: true, + Type: ReportOutputIncomingHtlc, Amount: finalAmt, MaturityHeight: h.htlcExpiry, LimboBalance: finalAmt, diff --git a/contractcourt/htlc_outgoing_contest_resolver.go b/contractcourt/htlc_outgoing_contest_resolver.go index 93a4adfa..28d95247 100644 --- a/contractcourt/htlc_outgoing_contest_resolver.go +++ b/contractcourt/htlc_outgoing_contest_resolver.go @@ -166,7 +166,7 @@ func (h *htlcOutgoingContestResolver) report() *ContractReport { return &ContractReport{ Outpoint: h.htlcResolution.ClaimOutpoint, - Incoming: false, + Type: ReportOutputOutgoingHtlc, Amount: finalAmt, MaturityHeight: h.htlcResolution.Expiry, LimboBalance: finalAmt, diff --git a/rpcserver.go b/rpcserver.go index 4833eb71..d9a083a9 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -34,6 +34,7 @@ import ( "github.com/lightningnetwork/lnd/chanbackup" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channelnotifier" + "github.com/lightningnetwork/lnd/contractcourt" "github.com/lightningnetwork/lnd/discovery" "github.com/lightningnetwork/lnd/htlcswitch" "github.com/lightningnetwork/lnd/input" @@ -2451,8 +2452,10 @@ func (r *rpcServer) arbitratorPopulateForceCloseResp(chanPoint *wire.OutPoint, reports := arbitrator.Report() for _, report := range reports { + incoming := report.Type == contractcourt.ReportOutputIncomingHtlc + htlc := &lnrpc.PendingHTLC{ - Incoming: report.Incoming, + Incoming: incoming, Amount: int64(report.Amount), Outpoint: report.Outpoint.String(), MaturityHeight: report.MaturityHeight, From 9acb2366656481c94f365e2e12d02f3129c4fb91 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Tue, 29 Oct 2019 17:23:08 +0100 Subject: [PATCH 5/8] cnct: remove nursery dependency in commit sweep resolver The channel arbitrator no longer passes the direct commitment output to the nursery for incubation. Instead the resolver itself will await the csv lock if any. The reason to change this now is to prevent having to deal with the (legacy) nursery code for a planned anchor outputs related change to the commit sweep resolver (also csv lock to_remote). It is no problem if there are any lingering incubating outputs at the time of upgrade. This just means that the output will be offered twice to the sweeper and this doesn't hurt. --- contractcourt/channel_arbitrator.go | 25 +-- contractcourt/commit_sweep_resolver.go | 287 ++++++++++++++----------- rpcserver.go | 59 +++-- 3 files changed, 208 insertions(+), 163 deletions(-) diff --git a/contractcourt/channel_arbitrator.go b/contractcourt/channel_arbitrator.go index 460e6a61..bb082170 100644 --- a/contractcourt/channel_arbitrator.go +++ b/contractcourt/channel_arbitrator.go @@ -141,6 +141,10 @@ const ( // ReportOutputOutgoingHtlc is an outgoing hash time locked contract on // the commitment tx. ReportOutputOutgoingHtlc + + // ReportOutputUnencumbered is an uncontested output on the commitment + // transaction paying to us directly. + ReportOutputUnencumbered ) // ContractReport provides a summary of a commitment tx output. @@ -873,27 +877,6 @@ func (c *ChannelArbitrator) stateStep( break } - // If we've have broadcast the commitment transaction, we send - // our commitment output for incubation, but only if it wasn't - // trimmed. We'll need to wait for a CSV timeout before we can - // reclaim the funds. - commitRes := contractResolutions.CommitResolution - if commitRes != nil && commitRes.MaturityDelay > 0 { - log.Infof("ChannelArbitrator(%v): sending commit "+ - "output for incubation", c.cfg.ChanPoint) - - err = c.cfg.IncubateOutputs( - c.cfg.ChanPoint, commitRes, - nil, nil, triggerHeight, - ) - if err != nil { - // TODO(roasbeef): check for AlreadyExists errors - log.Errorf("unable to incubate commitment "+ - "output: %v", err) - return StateError, closeTx, err - } - } - // Now that we know we'll need to act, we'll process the htlc // actions, wen create the structures we need to resolve all // outstanding contracts. diff --git a/contractcourt/commit_sweep_resolver.go b/contractcourt/commit_sweep_resolver.go index 874b1567..62a455f1 100644 --- a/contractcourt/commit_sweep_resolver.go +++ b/contractcourt/commit_sweep_resolver.go @@ -2,9 +2,12 @@ package contractcourt import ( "encoding/binary" + "fmt" "io" + "sync" "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/sweep" @@ -36,6 +39,13 @@ type commitSweepResolver struct { // chanPoint is the channel point of the original contract. chanPoint wire.OutPoint + // currentReport stores the current state of the resolver for reporting + // over the rpc interface. + currentReport ContractReport + + // reportLock prevents concurrent access to the resolver report. + reportLock sync.Mutex + contractResolverKit } @@ -52,6 +62,7 @@ func newCommitSweepResolver(res lnwallet.CommitOutputResolution, } r.initLogger(r) + r.initReport() return r } @@ -63,6 +74,63 @@ func (c *commitSweepResolver) ResolverKey() []byte { return key[:] } +// waitForHeight registers for block notifications and waits for the provided +// block height to be reached. +func (c *commitSweepResolver) waitForHeight(waitHeight uint32) error { + // Register for block epochs. After registration, the current height + // will be sent on the channel immediately. + blockEpochs, err := c.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 <-c.quit: + return errResolverShuttingDown + } + } +} + +// getCommitTxConfHeight waits for confirmation of the commitment tx and returns +// the confirmation height. +func (c *commitSweepResolver) getCommitTxConfHeight() (uint32, error) { + txID := c.commitResolution.SelfOutPoint.Hash + signDesc := c.commitResolution.SelfOutputSignDesc + pkScript := signDesc.Output.PkScript + const confDepth = 1 + confChan, err := c.Notifier.RegisterConfirmationsNtfn( + &txID, pkScript, confDepth, c.broadcastHeight, + ) + if err != nil { + return 0, err + } + defer confChan.Cancel() + + select { + case txConfirmation, ok := <-confChan.Confirmed: + if !ok { + return 0, fmt.Errorf("cannot get confirmation "+ + "for commit tx %v", txID) + } + + return txConfirmation.BlockHeight, nil + + case <-c.quit: + return 0, errResolverShuttingDown + } +} + // 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 first return value. In the case @@ -76,155 +144,100 @@ func (c *commitSweepResolver) Resolve() (ContractResolver, error) { return nil, nil } - // First, we'll register for a notification once the commitment output - // itself has been confirmed. - // - // TODO(roasbeef): instead sweep asap if remote commit? yeh - commitTXID := c.commitResolution.SelfOutPoint.Hash - sweepScript := c.commitResolution.SelfOutputSignDesc.Output.PkScript - confNtfn, err := c.Notifier.RegisterConfirmationsNtfn( - &commitTXID, sweepScript, 1, c.broadcastHeight, - ) + confHeight, err := c.getCommitTxConfHeight() if err != nil { return nil, err } - c.log.Debugf("waiting for commit tx to confirm") + unlockHeight := confHeight + c.commitResolution.MaturityDelay - select { - case _, ok := <-confNtfn.Confirmed: - if !ok { - return nil, errResolverShuttingDown + 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() + + // If there is a csv delay, we'll wait for that. + if c.commitResolution.MaturityDelay > 0 { + c.log.Debugf("waiting for csv lock to expire at height %v", + unlockHeight) + + // We only need to wait for the block before the block that + // unlocks the spend path. + err := c.waitForHeight(unlockHeight - 1) + if err != nil { + return nil, err } - - case <-c.quit: - return nil, errResolverShuttingDown } // We're dealing with our commitment transaction if the delay on the // resolution isn't zero. isLocalCommitTx := c.commitResolution.MaturityDelay != 0 - if !isLocalCommitTx { - // There're two types of commitments, those that have tweaks - // for the remote key (us in this case), and those that don't. - // We'll rely on the presence of the commitment tweak to to - // discern which type of commitment this is. - var witnessType input.WitnessType - if c.commitResolution.SelfOutputSignDesc.SingleTweak == nil { - witnessType = input.CommitSpendNoDelayTweakless - } else { - witnessType = input.CommitmentNoDelay - } - - // We'll craft an input with all the information required for - // the sweeper to create a fully valid sweeping transaction to - // recover these coins. - inp := input.MakeBaseInput( - &c.commitResolution.SelfOutPoint, - witnessType, - &c.commitResolution.SelfOutputSignDesc, - c.broadcastHeight, - ) - - // With our input constructed, we'll now offer it to the - // sweeper. - c.log.Infof("sweeping commit output") - - feePref := sweep.FeePreference{ConfTarget: commitOutputConfTarget} - resultChan, err := c.Sweeper.SweepInput(&inp, feePref) - if err != nil { - c.log.Errorf("unable to sweep input: %v", err) - - return nil, err - } - - // Sweeper is going to join this input with other inputs if - // possible and publish the sweep tx. When the sweep tx - // confirms, it signals us through the result channel with the - // outcome. Wait for this to happen. - select { - case sweepResult := <-resultChan: - if sweepResult.Err != nil { - c.log.Errorf("unable to sweep input: %v", - sweepResult.Err) - - return nil, sweepResult.Err - } - - c.log.Infof("commit tx fully resolved by sweep tx: %v", - sweepResult.Tx.TxHash()) - case <-c.quit: - return nil, errResolverShuttingDown - } - - c.resolved = true - return nil, c.Checkpoint(c) + // There're two types of commitments, those that have tweaks + // for the remote key (us in this case), and those that don't. + // We'll rely on the presence of the commitment tweak to to + // discern which type of commitment this is. + var witnessType input.WitnessType + switch { + case isLocalCommitTx: + witnessType = input.CommitmentTimeLock + case c.commitResolution.SelfOutputSignDesc.SingleTweak == nil: + witnessType = input.CommitSpendNoDelayTweakless + default: + witnessType = input.CommitmentNoDelay } - // Otherwise we are dealing with a local commitment transaction and the - // output we need to sweep has been sent to the nursery for incubation. - // In this case, we'll wait until the commitment output has been spent. - spendNtfn, err := c.Notifier.RegisterSpendNtfn( + // We'll craft an input with all the information required for + // the sweeper to create a fully valid sweeping transaction to + // recover these coins. + inp := input.NewCsvInput( &c.commitResolution.SelfOutPoint, - c.commitResolution.SelfOutputSignDesc.Output.PkScript, + witnessType, + &c.commitResolution.SelfOutputSignDesc, c.broadcastHeight, + c.commitResolution.MaturityDelay, ) + + // With our input constructed, we'll now offer it to the + // sweeper. + c.log.Infof("sweeping commit output") + + feePref := sweep.FeePreference{ConfTarget: commitOutputConfTarget} + resultChan, err := c.Sweeper.SweepInput(inp, feePref) if err != nil { + c.log.Errorf("unable to sweep input: %v", err) + return nil, err } - c.log.Infof("waiting for commit output to be swept") - - var sweepTx *wire.MsgTx + // Sweeper is going to join this input with other inputs if + // possible and publish the sweep tx. When the sweep tx + // confirms, it signals us through the result channel with the + // outcome. Wait for this to happen. select { - case commitSpend, ok := <-spendNtfn.Spend: - if !ok { - return nil, errResolverShuttingDown + case sweepResult := <-resultChan: + if sweepResult.Err != nil { + c.log.Errorf("unable to sweep input: %v", + sweepResult.Err) + + return nil, sweepResult.Err } - // Once we detect the commitment output has been spent, - // we'll extract the spending transaction itself, as we - // now consider this to be our sweep transaction. - sweepTx = commitSpend.SpendingTx - - c.log.Infof("commit output swept by txid=%v", sweepTx.TxHash()) - - if err := c.Checkpoint(c); err != nil { - c.log.Errorf("unable to Checkpoint: %v", err) - return nil, err - } + c.log.Infof("commit tx fully resolved by sweep tx: %v", + sweepResult.Tx.TxHash()) case <-c.quit: return nil, errResolverShuttingDown } - c.log.Infof("waiting for commit sweep txid=%v conf", sweepTx.TxHash()) + // Funds have been swept and balance is no longer in limbo. + c.reportLock.Lock() + c.currentReport.RecoveredBalance = c.currentReport.LimboBalance + c.currentReport.LimboBalance = 0 + c.reportLock.Unlock() - // Now we'll wait until the sweeping transaction has been fully - // confirmed. Once it's confirmed, we can mark this contract resolved. - sweepTXID := sweepTx.TxHash() - sweepingScript := sweepTx.TxOut[0].PkScript - confNtfn, err = c.Notifier.RegisterConfirmationsNtfn( - &sweepTXID, sweepingScript, 1, c.broadcastHeight, - ) - if err != nil { - return nil, err - } - select { - case confInfo, ok := <-confNtfn.Confirmed: - if !ok { - return nil, errResolverShuttingDown - } - - c.log.Infof("commit tx is fully resolved, at height: %v", - confInfo.BlockHeight) - - case <-c.quit: - return nil, errResolverShuttingDown - } - - // Once the transaction has received a sufficient number of - // confirmations, we'll mark ourselves as fully resolved and exit. c.resolved = true return nil, c.Checkpoint(c) } @@ -309,10 +322,42 @@ func newCommitSweepResolverFromReader(r io.Reader, resCfg ResolverConfig) ( // the database. c.initLogger(c) + c.initReport() return c, nil } +// report returns a report on the resolution state of the contract. +func (c *commitSweepResolver) report() *ContractReport { + c.reportLock.Lock() + defer c.reportLock.Unlock() + + copy := c.currentReport + return © +} + +// initReport initializes the pending channels report for this resolver. +func (c *commitSweepResolver) initReport() { + amt := btcutil.Amount( + c.commitResolution.SelfOutputSignDesc.Output.Value, + ) + + // Set the initial report. All fields are filled in, except for the + // maturity height which remains 0 until Resolve() is executed. + // + // TODO(joostjager): Resolvers only activate after the commit tx + // confirms. With more refactoring in channel arbitrator, it would be + // possible to make the confirmation height part of ResolverConfig and + // populate MaturityHeight here. + c.currentReport = ContractReport{ + Outpoint: c.commitResolution.SelfOutPoint, + Type: ReportOutputUnencumbered, + Amount: amt, + LimboBalance: amt, + RecoveredBalance: 0, + } +} + // A compile time assertion to ensure commitSweepResolver meets the // ContractResolver interface. -var _ ContractResolver = (*commitSweepResolver)(nil) +var _ reportingContractResolver = (*commitSweepResolver)(nil) diff --git a/rpcserver.go b/rpcserver.go index d9a083a9..7c87bad9 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -2452,25 +2452,51 @@ func (r *rpcServer) arbitratorPopulateForceCloseResp(chanPoint *wire.OutPoint, reports := arbitrator.Report() for _, report := range reports { - incoming := report.Type == contractcourt.ReportOutputIncomingHtlc + switch report.Type { - htlc := &lnrpc.PendingHTLC{ - Incoming: incoming, - Amount: int64(report.Amount), - Outpoint: report.Outpoint.String(), - MaturityHeight: report.MaturityHeight, - Stage: report.Stage, - } + // For a direct output, populate/update the top level + // response properties. + case contractcourt.ReportOutputUnencumbered: + // Populate the maturity height fields for the direct + // commitment output to us. + forceClose.MaturityHeight = report.MaturityHeight - if htlc.MaturityHeight != 0 { - htlc.BlocksTilMaturity = - int32(htlc.MaturityHeight) - currentHeight + // If the transaction has been confirmed, then we can + // compute how many blocks it has left. + if forceClose.MaturityHeight != 0 { + forceClose.BlocksTilMaturity = + int32(forceClose.MaturityHeight) - + currentHeight + } + + // Add htlcs to the PendingHtlcs response property. + case contractcourt.ReportOutputIncomingHtlc, + contractcourt.ReportOutputOutgoingHtlc: + + incoming := report.Type == contractcourt.ReportOutputIncomingHtlc + htlc := &lnrpc.PendingHTLC{ + Incoming: incoming, + Amount: int64(report.Amount), + Outpoint: report.Outpoint.String(), + MaturityHeight: report.MaturityHeight, + Stage: report.Stage, + } + + if htlc.MaturityHeight != 0 { + htlc.BlocksTilMaturity = + int32(htlc.MaturityHeight) - currentHeight + } + + forceClose.PendingHtlcs = append(forceClose.PendingHtlcs, htlc) + + default: + return fmt.Errorf("unknown report output type: %v", + report.Type) } forceClose.LimboBalance += int64(report.LimboBalance) forceClose.RecoveredBalance += int64(report.RecoveredBalance) - forceClose.PendingHtlcs = append(forceClose.PendingHtlcs, htlc) } return nil @@ -2501,15 +2527,6 @@ func (r *rpcServer) nurseryPopulateForceCloseResp(chanPoint *wire.OutPoint, // wallet. forceClose.LimboBalance = int64(nurseryInfo.limboBalance) forceClose.RecoveredBalance = int64(nurseryInfo.recoveredBalance) - forceClose.MaturityHeight = nurseryInfo.maturityHeight - - // If the transaction has been confirmed, then we can compute how many - // blocks it has left. - if forceClose.MaturityHeight != 0 { - forceClose.BlocksTilMaturity = - int32(forceClose.MaturityHeight) - - currentHeight - } for _, htlcReport := range nurseryInfo.htlcs { // TODO(conner) set incoming flag appropriately after handling From 919ab605409ff3ddcc4d334e8a66311c8c642fc4 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Wed, 30 Oct 2019 11:44:39 +0100 Subject: [PATCH 6/8] utxonursery: remove unused commit sweep code Now that the commit sweep resolver is no longer relying on the nursery, all code associated with commit sweeping can be removed. --- contractcourt/chain_arbitrator.go | 3 +- contractcourt/channel_arbitrator_test.go | 2 +- contractcourt/htlc_success_resolver.go | 2 +- contractcourt/htlc_timeout_resolver.go | 2 +- contractcourt/htlc_timeout_resolver_test.go | 1 - server.go | 3 +- utxonursery.go | 66 +-------------------- utxonursery_test.go | 54 ----------------- 8 files changed, 7 insertions(+), 126 deletions(-) diff --git a/contractcourt/chain_arbitrator.go b/contractcourt/chain_arbitrator.go index 3b25abc7..e30eec95 100644 --- a/contractcourt/chain_arbitrator.go +++ b/contractcourt/chain_arbitrator.go @@ -113,8 +113,7 @@ type ChainArbitratorConfig struct { // the process of incubation. This is used when a resolver wishes to // pass off the output to the nursery as we're only waiting on an // absolute/relative item block. - IncubateOutputs func(wire.OutPoint, *lnwallet.CommitOutputResolution, - *lnwallet.OutgoingHtlcResolution, + IncubateOutputs func(wire.OutPoint, *lnwallet.OutgoingHtlcResolution, *lnwallet.IncomingHtlcResolution, uint32) error // PreimageDB is a global store of all known pre-images. We'll use this diff --git a/contractcourt/channel_arbitrator_test.go b/contractcourt/channel_arbitrator_test.go index 59902873..af4681a3 100644 --- a/contractcourt/channel_arbitrator_test.go +++ b/contractcourt/channel_arbitrator_test.go @@ -301,7 +301,7 @@ func createTestChannelArbitrator(t *testing.T, log ArbitratorLog) (*chanArbTestC spendChan: make(chan *chainntnfs.SpendDetail), confChan: make(chan *chainntnfs.TxConfirmation), }, - IncubateOutputs: func(wire.OutPoint, *lnwallet.CommitOutputResolution, + IncubateOutputs: func(wire.OutPoint, *lnwallet.OutgoingHtlcResolution, *lnwallet.IncomingHtlcResolution, uint32) error { diff --git a/contractcourt/htlc_success_resolver.go b/contractcourt/htlc_success_resolver.go index 73d37063..c13c52e8 100644 --- a/contractcourt/htlc_success_resolver.go +++ b/contractcourt/htlc_success_resolver.go @@ -211,7 +211,7 @@ func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) { h, h.htlc.RHash[:]) err := h.IncubateOutputs( - h.ChanPoint, nil, nil, &h.htlcResolution, + h.ChanPoint, nil, &h.htlcResolution, h.broadcastHeight, ) if err != nil { diff --git a/contractcourt/htlc_timeout_resolver.go b/contractcourt/htlc_timeout_resolver.go index 2469f531..439babc4 100644 --- a/contractcourt/htlc_timeout_resolver.go +++ b/contractcourt/htlc_timeout_resolver.go @@ -247,7 +247,7 @@ func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) { h.htlcResolution.ClaimOutpoint) err := h.IncubateOutputs( - h.ChanPoint, nil, &h.htlcResolution, nil, + h.ChanPoint, &h.htlcResolution, nil, h.broadcastHeight, ) if err != nil { diff --git a/contractcourt/htlc_timeout_resolver_test.go b/contractcourt/htlc_timeout_resolver_test.go index 981cefc4..1e3daa59 100644 --- a/contractcourt/htlc_timeout_resolver_test.go +++ b/contractcourt/htlc_timeout_resolver_test.go @@ -216,7 +216,6 @@ func TestHtlcTimeoutResolver(t *testing.T) { Notifier: notifier, PreimageDB: witnessBeacon, IncubateOutputs: func(wire.OutPoint, - *lnwallet.CommitOutputResolution, *lnwallet.OutgoingHtlcResolution, *lnwallet.IncomingHtlcResolution, uint32) error { diff --git a/server.go b/server.go index ccdd13ae..c4c997f5 100644 --- a/server.go +++ b/server.go @@ -847,7 +847,6 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB, return nil }, IncubateOutputs: func(chanPoint wire.OutPoint, - commitRes *lnwallet.CommitOutputResolution, outHtlcRes *lnwallet.OutgoingHtlcResolution, inHtlcRes *lnwallet.IncomingHtlcResolution, broadcastHeight uint32) error { @@ -864,7 +863,7 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB, } return s.utxoNursery.IncubateOutputs( - chanPoint, commitRes, outRes, inRes, + chanPoint, outRes, inRes, broadcastHeight, ) }, diff --git a/utxonursery.go b/utxonursery.go index b08cf0e2..2e749130 100644 --- a/utxonursery.go +++ b/utxonursery.go @@ -330,7 +330,6 @@ func (u *utxoNursery) Stop() error { // they're CLTV absolute time locked, or if they're CSV relative time locked. // Once all outputs reach maturity, they'll be swept back into the wallet. func (u *utxoNursery) IncubateOutputs(chanPoint wire.OutPoint, - commitResolution *lnwallet.CommitOutputResolution, outgoingHtlcs []lnwallet.OutgoingHtlcResolution, incomingHtlcs []lnwallet.IncomingHtlcResolution, broadcastHeight uint32) error { @@ -352,8 +351,6 @@ func (u *utxoNursery) IncubateOutputs(chanPoint wire.OutPoint, numHtlcs := len(incomingHtlcs) + len(outgoingHtlcs) var ( - hasCommit bool - // Kid outputs can be swept after an initial confirmation // followed by a maturity period.Baby outputs are two stage and // will need to wait for an absolute time out to reach a @@ -364,28 +361,6 @@ func (u *utxoNursery) IncubateOutputs(chanPoint wire.OutPoint, // 1. Build all the spendable outputs that we will try to incubate. - // It could be that our to-self output was below the dust limit. In - // that case the commit resolution would be nil and we would not have - // that output to incubate. - if commitResolution != nil { - hasCommit = true - selfOutput := makeKidOutput( - &commitResolution.SelfOutPoint, - &chanPoint, - commitResolution.MaturityDelay, - input.CommitmentTimeLock, - &commitResolution.SelfOutputSignDesc, - 0, - ) - - // We'll skip any zero valued outputs as this indicates we - // don't have a settled balance within the commitment - // transaction. - if selfOutput.Amount() > 0 { - kidOutputs = append(kidOutputs, selfOutput) - } - } - // TODO(roasbeef): query and see if we already have, if so don't add? // For each incoming HTLC, we'll register a kid output marked as a @@ -436,8 +411,8 @@ func (u *utxoNursery) IncubateOutputs(chanPoint wire.OutPoint, // * need ability to cancel in the case that we learn of pre-image or // remote party pulls - utxnLog.Infof("Incubating Channel(%s) has-commit=%v, num-htlcs=%d", - chanPoint, hasCommit, numHtlcs) + utxnLog.Infof("Incubating Channel(%s) num-htlcs=%d", + chanPoint, numHtlcs) u.mu.Lock() defer u.mu.Unlock() @@ -538,8 +513,6 @@ func (u *utxoNursery) NurseryReport( // Preschool outputs are awaiting the // confirmation of the commitment transaction. switch kid.WitnessType() { - case input.CommitmentTimeLock: - report.AddLimboCommitment(&kid) case input.HtlcAcceptedSuccessSecondLevel: // An HTLC output on our commitment transaction @@ -561,11 +534,6 @@ func (u *utxoNursery) NurseryReport( // We can distinguish them via their witness // types. switch kid.WitnessType() { - case input.CommitmentTimeLock: - // The commitment transaction has been - // confirmed, and we are waiting the CSV - // delay to expire. - report.AddLimboCommitment(&kid) case input.HtlcOfferedRemoteTimeout: // This is an HTLC output on the @@ -590,11 +558,6 @@ func (u *utxoNursery) NurseryReport( // will contribute towards the recovered // balance. switch kid.WitnessType() { - case input.CommitmentTimeLock: - // The commitment output was - // successfully swept back into a - // regular p2wkh output. - report.AddRecoveredCommitment(&kid) case input.HtlcAcceptedSuccessSecondLevel: fallthrough @@ -1071,11 +1034,6 @@ type contractMaturityReport struct { // recoveredBalance is the total value that has been successfully swept // back to the user's wallet. recoveredBalance btcutil.Amount - - // maturityHeight is the absolute block height that this output will - // mature at. - maturityHeight uint32 - // htlcs records a maturity report for each htlc output in this channel. htlcs []htlcMaturityReport } @@ -1100,26 +1058,6 @@ type htlcMaturityReport struct { stage uint32 } -// AddLimboCommitment adds an incubating commitment output to maturity -// report's htlcs, and contributes its amount to the limbo balance. -func (c *contractMaturityReport) AddLimboCommitment(kid *kidOutput) { - c.limboBalance += kid.Amount() - - // If the confirmation height is set, then this means the contract has - // been confirmed, and we know the final maturity height. - if kid.ConfHeight() != 0 { - c.maturityHeight = kid.BlocksToMaturity() + kid.ConfHeight() - } -} - -// AddRecoveredCommitment adds a graduated commitment output to maturity -// report's htlcs, and contributes its amount to the recovered balance. -func (c *contractMaturityReport) AddRecoveredCommitment(kid *kidOutput) { - c.recoveredBalance += kid.Amount() - - c.maturityHeight = kid.BlocksToMaturity() + kid.ConfHeight() -} - // AddLimboStage1TimeoutHtlc adds an htlc crib output to the maturity report's // htlcs, and contributes its amount to the limbo balance. func (c *contractMaturityReport) AddLimboStage1TimeoutHtlc(baby *babyOutput) { diff --git a/utxonursery_test.go b/utxonursery_test.go index 6671c070..6b0d6de0 100644 --- a/utxonursery_test.go +++ b/utxonursery_test.go @@ -650,7 +650,6 @@ func incubateTestOutput(t *testing.T, nursery *utxoNursery, // Hand off to nursery. err := nursery.IncubateOutputs( testChanPoint, - nil, []lnwallet.OutgoingHtlcResolution{*outgoingRes}, nil, 0, ) @@ -839,59 +838,6 @@ func testNurseryOutgoingHtlcSuccessOnRemote(t *testing.T, ctx.finish() } -func TestNurseryCommitSuccessOnLocal(t *testing.T) { - testRestartLoop(t, testNurseryCommitSuccessOnLocal) -} - -func testNurseryCommitSuccessOnLocal(t *testing.T, - checkStartStop func(func()) bool) { - - ctx := createNurseryTestContext(t, checkStartStop) - - commitRes := createCommitmentRes() - - // Hand off to nursery. - err := ctx.nursery.IncubateOutputs( - testChanPoint, - commitRes, nil, nil, 0, - ) - if err != nil { - t.Fatal(err) - } - - // Verify that commitment output is showing up in nursery report as - // limbo balance. - assertNurseryReport(t, ctx.nursery, 0, 0, 10000) - - ctx.restart() - - // Notify confirmation of the commitment tx. - err = ctx.notifier.ConfirmTx(&commitRes.SelfOutPoint.Hash, 124) - if err != nil { - t.Fatal(err) - } - - // Wait for output to be promoted from PSCL to KNDR. - select { - case <-ctx.store.preschoolToKinderChan: - case <-time.After(defaultTestTimeout): - t.Fatalf("output not promoted to KNDR") - } - - ctx.restart() - - // Notify arrival of block where commit output CSV expires. - ctx.notifyEpoch(126) - - // Check final sweep into wallet. - testSweep(t, ctx, func() { - // Check limbo balance after sweep publication - assertNurseryReport(t, ctx.nursery, 0, 0, 10000) - }) - - ctx.finish() -} - func testSweepHtlc(t *testing.T, ctx *nurseryTestContext) { testSweep(t, ctx, func() { // Verify stage in nursery report. HTLCs should now both still From 08c9db9725c342eb04d0f33e1ad424b218def38c Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Thu, 31 Oct 2019 12:42:49 +0100 Subject: [PATCH 7/8] cnct: create sweeper interface --- contractcourt/chain_arbitrator.go | 3 +-- contractcourt/interfaces.go | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/contractcourt/chain_arbitrator.go b/contractcourt/chain_arbitrator.go index e30eec95..6d7c674a 100644 --- a/contractcourt/chain_arbitrator.go +++ b/contractcourt/chain_arbitrator.go @@ -15,7 +15,6 @@ import ( "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwire" - "github.com/lightningnetwork/lnd/sweep" ) // ErrChainArbExiting signals that the chain arbitrator is shutting down. @@ -141,7 +140,7 @@ type ChainArbitratorConfig struct { DisableChannel func(wire.OutPoint) error // Sweeper allows resolvers to sweep their final outputs. - Sweeper *sweep.UtxoSweeper + Sweeper UtxoSweeper // Registry is the invoice database that is used by resolvers to lookup // preimages and settle invoices. diff --git a/contractcourt/interfaces.go b/contractcourt/interfaces.go index a8847785..682eb2ac 100644 --- a/contractcourt/interfaces.go +++ b/contractcourt/interfaces.go @@ -3,11 +3,14 @@ package contractcourt import ( "io" + "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/htlcswitch/hop" + "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/invoices" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/sweep" ) // Registry is an interface which represents the invoice registry. @@ -36,3 +39,16 @@ type OnionProcessor interface { // the passed io.Reader instance. ReconstructHopIterator(r io.Reader, rHash []byte) (hop.Iterator, error) } + +// UtxoSweeper defines the sweep functions that contract court requires. +type UtxoSweeper interface { + // SweepInput sweeps inputs back into the wallet. + SweepInput(input input.Input, + feePreference sweep.FeePreference) (chan sweep.Result, error) + + // CreateSweepTx accepts a list of inputs and signs and generates a txn + // that spends from them. This method also makes an accurate fee + // estimate before generating the required witnesses. + CreateSweepTx(inputs []input.Input, feePref sweep.FeePreference, + currentBlockHeight uint32) (*wire.MsgTx, error) +} From 462d86d0bb97faa88541cbb41c3ca09bc9eb087a Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Wed, 30 Oct 2019 13:07:26 +0100 Subject: [PATCH 8/8] cnct/test: add unit test for commit sweep resolver --- contractcourt/chain_watcher_test.go | 1 + contractcourt/commit_sweep_resolver_test.go | 225 ++++++++++++++++++++ 2 files changed, 226 insertions(+) create mode 100644 contractcourt/commit_sweep_resolver_test.go diff --git a/contractcourt/chain_watcher_test.go b/contractcourt/chain_watcher_test.go index f0e07628..466662c1 100644 --- a/contractcourt/chain_watcher_test.go +++ b/contractcourt/chain_watcher_test.go @@ -26,6 +26,7 @@ func (m *mockNotifier) RegisterConfirmationsNtfn(txid *chainhash.Hash, _ []byte, heightHint uint32) (*chainntnfs.ConfirmationEvent, error) { return &chainntnfs.ConfirmationEvent{ Confirmed: m.confChan, + Cancel: func() {}, }, nil } diff --git a/contractcourt/commit_sweep_resolver_test.go b/contractcourt/commit_sweep_resolver_test.go new file mode 100644 index 00000000..957e0802 --- /dev/null +++ b/contractcourt/commit_sweep_resolver_test.go @@ -0,0 +1,225 @@ +package contractcourt + +import ( + "reflect" + "testing" + "time" + + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/lightningnetwork/lnd/chainntnfs" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/sweep" +) + +type commitSweepResolverTestContext struct { + resolver *commitSweepResolver + notifier *mockNotifier + sweeper *mockSweeper + resolverResultChan chan resolveResult + t *testing.T +} + +func newCommitSweepResolverTestContext(t *testing.T, + resolution *lnwallet.CommitOutputResolution) *commitSweepResolverTestContext { + + notifier := &mockNotifier{ + epochChan: make(chan *chainntnfs.BlockEpoch), + spendChan: make(chan *chainntnfs.SpendDetail), + confChan: make(chan *chainntnfs.TxConfirmation), + } + + sweeper := newMockSweeper() + + checkPointChan := make(chan struct{}, 1) + + chainCfg := ChannelArbitratorConfig{ + ChainArbitratorConfig: ChainArbitratorConfig{ + Notifier: notifier, + Sweeper: sweeper, + }, + } + + cfg := ResolverConfig{ + ChannelArbitratorConfig: chainCfg, + Checkpoint: func(_ ContractResolver) error { + checkPointChan <- struct{}{} + return nil + }, + } + + resolver := newCommitSweepResolver( + *resolution, 0, wire.OutPoint{}, cfg, + ) + + return &commitSweepResolverTestContext{ + resolver: resolver, + notifier: notifier, + sweeper: sweeper, + t: t, + } +} + +func (i *commitSweepResolverTestContext) resolve() { + // Start resolver. + i.resolverResultChan = make(chan resolveResult, 1) + go func() { + nextResolver, err := i.resolver.Resolve() + i.resolverResultChan <- resolveResult{ + nextResolver: nextResolver, + err: err, + } + }() +} + +func (i *commitSweepResolverTestContext) notifyEpoch(height int32) { + i.notifier.epochChan <- &chainntnfs.BlockEpoch{ + Height: height, + } +} + +func (i *commitSweepResolverTestContext) waitForResult() { + i.t.Helper() + + result := <-i.resolverResultChan + if result.err != nil { + i.t.Fatal(result.err) + } + + if result.nextResolver != nil { + i.t.Fatal("expected no next resolver") + } +} + +type mockSweeper struct { + sweptInputs chan input.Input +} + +func newMockSweeper() *mockSweeper { + return &mockSweeper{ + sweptInputs: make(chan input.Input), + } +} + +func (s *mockSweeper) SweepInput(input input.Input, + feePreference sweep.FeePreference) (chan sweep.Result, error) { + + s.sweptInputs <- input + + result := make(chan sweep.Result, 1) + result <- sweep.Result{ + Tx: &wire.MsgTx{}, + } + return result, nil +} + +func (s *mockSweeper) CreateSweepTx(inputs []input.Input, feePref sweep.FeePreference, + currentBlockHeight uint32) (*wire.MsgTx, error) { + + return nil, nil +} + +var _ UtxoSweeper = &mockSweeper{} + +// TestCommitSweepResolverNoDelay tests resolution of a direct commitment output +// unencumbered by a time lock. +func TestCommitSweepResolverNoDelay(t *testing.T) { + t.Parallel() + defer timeout(t)() + + res := lnwallet.CommitOutputResolution{ + SelfOutputSignDesc: input.SignDescriptor{ + Output: &wire.TxOut{ + Value: 100, + }, + }, + } + + ctx := newCommitSweepResolverTestContext(t, &res) + ctx.resolve() + + ctx.notifier.confChan <- &chainntnfs.TxConfirmation{} + + // No csv delay, so the input should be swept immediately. + <-ctx.sweeper.sweptInputs + + ctx.waitForResult() +} + +// TestCommitSweepResolverDelay tests resolution of a direct commitment output +// that is encumbered by a time lock. +func TestCommitSweepResolverDelay(t *testing.T) { + t.Parallel() + defer timeout(t)() + + amt := int64(100) + outpoint := wire.OutPoint{ + Index: 5, + } + res := lnwallet.CommitOutputResolution{ + SelfOutputSignDesc: input.SignDescriptor{ + Output: &wire.TxOut{ + Value: amt, + }, + }, + MaturityDelay: 3, + SelfOutPoint: outpoint, + } + + ctx := newCommitSweepResolverTestContext(t, &res) + + report := ctx.resolver.report() + if !reflect.DeepEqual(report, &ContractReport{ + Outpoint: outpoint, + Type: ReportOutputUnencumbered, + Amount: btcutil.Amount(amt), + LimboBalance: btcutil.Amount(amt), + }) { + t.Fatal("unexpected resolver report") + } + + ctx.resolve() + + ctx.notifier.confChan <- &chainntnfs.TxConfirmation{ + BlockHeight: testInitialBlockHeight - 1, + } + + // Allow resolver to process confirmation. + time.Sleep(100 * time.Millisecond) + + // Expect report to be updated. + report = ctx.resolver.report() + if report.MaturityHeight != testInitialBlockHeight+2 { + 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(100 * time.Millisecond): + } + + // 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 + + ctx.waitForResult() + + report = ctx.resolver.report() + if !reflect.DeepEqual(report, &ContractReport{ + Outpoint: outpoint, + Type: ReportOutputUnencumbered, + Amount: btcutil.Amount(amt), + RecoveredBalance: btcutil.Amount(amt), + MaturityHeight: testInitialBlockHeight + 2, + }) { + t.Fatal("unexpected resolver report") + } +}