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.
This commit is contained in:
Joost Jager 2019-10-29 17:23:08 +01:00
parent 1597a92160
commit 9acb236665
No known key found for this signature in database
GPG Key ID: A61B9D4C393C59C7
3 changed files with 208 additions and 163 deletions

@ -141,6 +141,10 @@ const (
// ReportOutputOutgoingHtlc is an outgoing hash time locked contract on // ReportOutputOutgoingHtlc is an outgoing hash time locked contract on
// the commitment tx. // the commitment tx.
ReportOutputOutgoingHtlc ReportOutputOutgoingHtlc
// ReportOutputUnencumbered is an uncontested output on the commitment
// transaction paying to us directly.
ReportOutputUnencumbered
) )
// ContractReport provides a summary of a commitment tx output. // ContractReport provides a summary of a commitment tx output.
@ -873,27 +877,6 @@ func (c *ChannelArbitrator) stateStep(
break 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 // Now that we know we'll need to act, we'll process the htlc
// actions, wen create the structures we need to resolve all // actions, wen create the structures we need to resolve all
// outstanding contracts. // outstanding contracts.

@ -2,9 +2,12 @@ package contractcourt
import ( import (
"encoding/binary" "encoding/binary"
"fmt"
"io" "io"
"sync"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/sweep" "github.com/lightningnetwork/lnd/sweep"
@ -36,6 +39,13 @@ type commitSweepResolver struct {
// chanPoint is the channel point of the original contract. // chanPoint is the channel point of the original contract.
chanPoint wire.OutPoint 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 contractResolverKit
} }
@ -52,6 +62,7 @@ func newCommitSweepResolver(res lnwallet.CommitOutputResolution,
} }
r.initLogger(r) r.initLogger(r)
r.initReport()
return r return r
} }
@ -63,6 +74,63 @@ func (c *commitSweepResolver) ResolverKey() []byte {
return key[:] 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 // Resolve instructs the contract resolver to resolve the output on-chain. Once
// the output has been *fully* resolved, the function should return immediately // the output has been *fully* resolved, the function should return immediately
// with a nil ContractResolver value for the first return value. In the case // 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 return nil, nil
} }
// First, we'll register for a notification once the commitment output confHeight, err := c.getCommitTxConfHeight()
// 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,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
c.log.Debugf("waiting for commit tx to confirm") unlockHeight := confHeight + c.commitResolution.MaturityDelay
select { c.log.Debugf("commit conf_height=%v, unlock_height=%v",
case _, ok := <-confNtfn.Confirmed: confHeight, unlockHeight)
if !ok {
return nil, errResolverShuttingDown // 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 // We're dealing with our commitment transaction if the delay on the
// resolution isn't zero. // resolution isn't zero.
isLocalCommitTx := c.commitResolution.MaturityDelay != 0 isLocalCommitTx := c.commitResolution.MaturityDelay != 0
if !isLocalCommitTx { // There're two types of commitments, those that have tweaks
// There're two types of commitments, those that have tweaks // for the remote key (us in this case), and those that don't.
// 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
// We'll rely on the presence of the commitment tweak to to // discern which type of commitment this is.
// discern which type of commitment this is. var witnessType input.WitnessType
var witnessType input.WitnessType switch {
if c.commitResolution.SelfOutputSignDesc.SingleTweak == nil { case isLocalCommitTx:
witnessType = input.CommitSpendNoDelayTweakless witnessType = input.CommitmentTimeLock
} else { case c.commitResolution.SelfOutputSignDesc.SingleTweak == nil:
witnessType = input.CommitmentNoDelay witnessType = input.CommitSpendNoDelayTweakless
} default:
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)
} }
// Otherwise we are dealing with a local commitment transaction and the // We'll craft an input with all the information required for
// output we need to sweep has been sent to the nursery for incubation. // the sweeper to create a fully valid sweeping transaction to
// In this case, we'll wait until the commitment output has been spent. // recover these coins.
spendNtfn, err := c.Notifier.RegisterSpendNtfn( inp := input.NewCsvInput(
&c.commitResolution.SelfOutPoint, &c.commitResolution.SelfOutPoint,
c.commitResolution.SelfOutputSignDesc.Output.PkScript, witnessType,
&c.commitResolution.SelfOutputSignDesc,
c.broadcastHeight, 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 { if err != nil {
c.log.Errorf("unable to sweep input: %v", err)
return nil, err return nil, err
} }
c.log.Infof("waiting for commit output to be swept") // Sweeper is going to join this input with other inputs if
// possible and publish the sweep tx. When the sweep tx
var sweepTx *wire.MsgTx // confirms, it signals us through the result channel with the
// outcome. Wait for this to happen.
select { select {
case commitSpend, ok := <-spendNtfn.Spend: case sweepResult := <-resultChan:
if !ok { if sweepResult.Err != nil {
return nil, errResolverShuttingDown c.log.Errorf("unable to sweep input: %v",
sweepResult.Err)
return nil, sweepResult.Err
} }
// Once we detect the commitment output has been spent, c.log.Infof("commit tx fully resolved by sweep tx: %v",
// we'll extract the spending transaction itself, as we sweepResult.Tx.TxHash())
// 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
}
case <-c.quit: case <-c.quit:
return nil, errResolverShuttingDown 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 c.resolved = true
return nil, c.Checkpoint(c) return nil, c.Checkpoint(c)
} }
@ -309,10 +322,42 @@ func newCommitSweepResolverFromReader(r io.Reader, resCfg ResolverConfig) (
// the database. // the database.
c.initLogger(c) c.initLogger(c)
c.initReport()
return c, nil 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 &copy
}
// 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 // A compile time assertion to ensure commitSweepResolver meets the
// ContractResolver interface. // ContractResolver interface.
var _ ContractResolver = (*commitSweepResolver)(nil) var _ reportingContractResolver = (*commitSweepResolver)(nil)

@ -2452,25 +2452,51 @@ func (r *rpcServer) arbitratorPopulateForceCloseResp(chanPoint *wire.OutPoint,
reports := arbitrator.Report() reports := arbitrator.Report()
for _, report := range reports { for _, report := range reports {
incoming := report.Type == contractcourt.ReportOutputIncomingHtlc switch report.Type {
htlc := &lnrpc.PendingHTLC{ // For a direct output, populate/update the top level
Incoming: incoming, // response properties.
Amount: int64(report.Amount), case contractcourt.ReportOutputUnencumbered:
Outpoint: report.Outpoint.String(), // Populate the maturity height fields for the direct
MaturityHeight: report.MaturityHeight, // commitment output to us.
Stage: report.Stage, forceClose.MaturityHeight = report.MaturityHeight
}
if htlc.MaturityHeight != 0 { // If the transaction has been confirmed, then we can
htlc.BlocksTilMaturity = // compute how many blocks it has left.
int32(htlc.MaturityHeight) - currentHeight 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.LimboBalance += int64(report.LimboBalance)
forceClose.RecoveredBalance += int64(report.RecoveredBalance) forceClose.RecoveredBalance += int64(report.RecoveredBalance)
forceClose.PendingHtlcs = append(forceClose.PendingHtlcs, htlc)
} }
return nil return nil
@ -2501,15 +2527,6 @@ func (r *rpcServer) nurseryPopulateForceCloseResp(chanPoint *wire.OutPoint,
// wallet. // wallet.
forceClose.LimboBalance = int64(nurseryInfo.limboBalance) forceClose.LimboBalance = int64(nurseryInfo.limboBalance)
forceClose.RecoveredBalance = int64(nurseryInfo.recoveredBalance) 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 { for _, htlcReport := range nurseryInfo.htlcs {
// TODO(conner) set incoming flag appropriately after handling // TODO(conner) set incoming flag appropriately after handling