Merge pull request #3644 from joostjager/commit-sweep-no-nursery

cnct: commit sweep without nursery
This commit is contained in:
Johan T. Halseth 2019-11-15 10:38:57 +01:00 committed by GitHub
commit 840051cc3d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 526 additions and 318 deletions

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

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

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

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

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

@ -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")
}
}

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

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

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

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

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

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

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

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

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

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

@ -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,
)
},

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

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