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

View File

@ -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.

View File

@ -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 &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
// ContractResolver interface.
var _ ContractResolver = (*commitSweepResolver)(nil)
var _ reportingContractResolver = (*commitSweepResolver)(nil)

View File

@ -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