diff --git a/contractcourt/chain_arbitrator.go b/contractcourt/chain_arbitrator.go index 3b25abc7..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. @@ -113,8 +112,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 @@ -142,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/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/channel_arbitrator.go b/contractcourt/channel_arbitrator.go index e63a2046..bb082170 100644 --- a/contractcourt/channel_arbitrator.go +++ b/contractcourt/channel_arbitrator.go @@ -129,13 +129,31 @@ 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 + + // ReportOutputUnencumbered is an uncontested output on the commitment + // transaction paying to us directly. + ReportOutputUnencumbered +) + // 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 @@ -859,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/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/commit_sweep_resolver.go b/contractcourt/commit_sweep_resolver.go index 3b7f3b85..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 } @@ -44,12 +54,17 @@ 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) + r.initReport() + + return r } // ResolverKey returns an identifier which should be globally unique for this @@ -59,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 @@ -72,159 +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 } - log.Debugf("%T(%v): waiting for commit tx to confirm", c, c.chanPoint) + 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. - log.Infof("%T(%v): sweeping commit output", c, c.chanPoint) - - 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) - - 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 { - log.Errorf("%T(%v): unable to sweep input: %v", - c, c.chanPoint, sweepResult.Err) - - return nil, sweepResult.Err - } - - log.Infof("ChannelPoint(%v) commit tx is fully resolved by "+ - "sweep tx: %v", c.chanPoint, 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 } - log.Infof("%T(%v): waiting for commit output to be swept", c, - c.chanPoint) - - 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 - - log.Infof("%T(%v): commit output swept by txid=%v", - c, c.chanPoint, sweepTx.TxHash()) - - if err := c.Checkpoint(c); err != nil { - 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 } - log.Infof("%T(%v): waiting for commit sweep txid=%v conf", c, c.chanPoint, - 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 - } - - log.Infof("ChannelPoint(%v) commit tx is fully resolved, at height: %v", - c.chanPoint, 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) } @@ -308,9 +321,43 @@ 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.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/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") + } +} 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. 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_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 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/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/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) +} 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) diff --git a/rpcserver.go b/rpcserver.go index c3bc50a0..13cb66b1 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,23 +2452,51 @@ func (r *rpcServer) arbitratorPopulateForceCloseResp(chanPoint *wire.OutPoint, reports := arbitrator.Report() for _, report := range reports { - htlc := &lnrpc.PendingHTLC{ - Incoming: report.Incoming, - Amount: int64(report.Amount), - Outpoint: report.Outpoint.String(), - MaturityHeight: report.MaturityHeight, - Stage: report.Stage, - } + switch report.Type { - if htlc.MaturityHeight != 0 { - htlc.BlocksTilMaturity = - int32(htlc.MaturityHeight) - currentHeight + // 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 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 @@ -2498,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 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