Merge pull request #3644 from joostjager/commit-sweep-no-nursery
cnct: commit sweep without nursery
This commit is contained in:
commit
840051cc3d
@ -15,7 +15,6 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/lnwallet"
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
"github.com/lightningnetwork/lnd/sweep"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrChainArbExiting signals that the chain arbitrator is shutting down.
|
// 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
|
// 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
|
// pass off the output to the nursery as we're only waiting on an
|
||||||
// absolute/relative item block.
|
// absolute/relative item block.
|
||||||
IncubateOutputs func(wire.OutPoint, *lnwallet.CommitOutputResolution,
|
IncubateOutputs func(wire.OutPoint, *lnwallet.OutgoingHtlcResolution,
|
||||||
*lnwallet.OutgoingHtlcResolution,
|
|
||||||
*lnwallet.IncomingHtlcResolution, uint32) error
|
*lnwallet.IncomingHtlcResolution, uint32) error
|
||||||
|
|
||||||
// PreimageDB is a global store of all known pre-images. We'll use this
|
// 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
|
DisableChannel func(wire.OutPoint) error
|
||||||
|
|
||||||
// Sweeper allows resolvers to sweep their final outputs.
|
// 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
|
// Registry is the invoice database that is used by resolvers to lookup
|
||||||
// preimages and settle invoices.
|
// preimages and settle invoices.
|
||||||
|
@ -26,6 +26,7 @@ func (m *mockNotifier) RegisterConfirmationsNtfn(txid *chainhash.Hash, _ []byte,
|
|||||||
heightHint uint32) (*chainntnfs.ConfirmationEvent, error) {
|
heightHint uint32) (*chainntnfs.ConfirmationEvent, error) {
|
||||||
return &chainntnfs.ConfirmationEvent{
|
return &chainntnfs.ConfirmationEvent{
|
||||||
Confirmed: m.confChan,
|
Confirmed: m.confChan,
|
||||||
|
Cancel: func() {},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,13 +129,31 @@ type ChannelArbitratorConfig struct {
|
|||||||
ChainArbitratorConfig
|
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.
|
// ContractReport provides a summary of a commitment tx output.
|
||||||
type ContractReport struct {
|
type ContractReport struct {
|
||||||
// Outpoint is the final output that will be swept back to the wallet.
|
// Outpoint is the final output that will be swept back to the wallet.
|
||||||
Outpoint wire.OutPoint
|
Outpoint wire.OutPoint
|
||||||
|
|
||||||
// Incoming indicates whether the htlc was incoming to this channel.
|
// Type indicates the type of the reported output.
|
||||||
Incoming bool
|
Type ReportOutputType
|
||||||
|
|
||||||
// Amount is the final value that will be swept in back to the wallet.
|
// Amount is the final value that will be swept in back to the wallet.
|
||||||
Amount btcutil.Amount
|
Amount btcutil.Amount
|
||||||
@ -859,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.
|
||||||
|
@ -301,7 +301,7 @@ func createTestChannelArbitrator(t *testing.T, log ArbitratorLog) (*chanArbTestC
|
|||||||
spendChan: make(chan *chainntnfs.SpendDetail),
|
spendChan: make(chan *chainntnfs.SpendDetail),
|
||||||
confChan: make(chan *chainntnfs.TxConfirmation),
|
confChan: make(chan *chainntnfs.TxConfirmation),
|
||||||
},
|
},
|
||||||
IncubateOutputs: func(wire.OutPoint, *lnwallet.CommitOutputResolution,
|
IncubateOutputs: func(wire.OutPoint,
|
||||||
*lnwallet.OutgoingHtlcResolution,
|
*lnwallet.OutgoingHtlcResolution,
|
||||||
*lnwallet.IncomingHtlcResolution, uint32) error {
|
*lnwallet.IncomingHtlcResolution, uint32) error {
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,12 +54,17 @@ func newCommitSweepResolver(res lnwallet.CommitOutputResolution,
|
|||||||
broadcastHeight uint32,
|
broadcastHeight uint32,
|
||||||
chanPoint wire.OutPoint, resCfg ResolverConfig) *commitSweepResolver {
|
chanPoint wire.OutPoint, resCfg ResolverConfig) *commitSweepResolver {
|
||||||
|
|
||||||
return &commitSweepResolver{
|
r := &commitSweepResolver{
|
||||||
contractResolverKit: *newContractResolverKit(resCfg),
|
contractResolverKit: *newContractResolverKit(resCfg),
|
||||||
commitResolution: res,
|
commitResolution: res,
|
||||||
broadcastHeight: broadcastHeight,
|
broadcastHeight: broadcastHeight,
|
||||||
chanPoint: chanPoint,
|
chanPoint: chanPoint,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
r.initLogger(r)
|
||||||
|
r.initReport()
|
||||||
|
|
||||||
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolverKey returns an identifier which should be globally unique for this
|
// ResolverKey returns an identifier which should be globally unique for this
|
||||||
@ -59,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
|
||||||
@ -72,159 +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
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("%T(%v): waiting for commit tx to confirm", c, c.chanPoint)
|
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.
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("%T(%v): waiting for commit output to be swept", c,
|
// Sweeper is going to join this input with other inputs if
|
||||||
c.chanPoint)
|
// possible and publish the sweep tx. When the sweep tx
|
||||||
|
// confirms, it signals us through the result channel with the
|
||||||
var sweepTx *wire.MsgTx
|
// 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
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
case <-c.quit:
|
case <-c.quit:
|
||||||
return nil, errResolverShuttingDown
|
return nil, errResolverShuttingDown
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("%T(%v): waiting for commit sweep txid=%v conf", c, c.chanPoint,
|
// Funds have been swept and balance is no longer in limbo.
|
||||||
sweepTx.TxHash())
|
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
|
c.resolved = true
|
||||||
return nil, c.Checkpoint(c)
|
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
|
// removed this, but keep in mind that this data may still be present in
|
||||||
// the database.
|
// the database.
|
||||||
|
|
||||||
|
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 ©
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
225
contractcourt/commit_sweep_resolver_test.go
Normal file
225
contractcourt/commit_sweep_resolver_test.go
Normal file
@ -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 (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/btcsuite/btclog"
|
||||||
|
"github.com/lightningnetwork/lnd/build"
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -93,6 +96,8 @@ type ResolverConfig struct {
|
|||||||
type contractResolverKit struct {
|
type contractResolverKit struct {
|
||||||
ResolverConfig
|
ResolverConfig
|
||||||
|
|
||||||
|
log btclog.Logger
|
||||||
|
|
||||||
quit chan struct{}
|
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 (
|
var (
|
||||||
// errResolverShuttingDown is returned when the resolver stops
|
// errResolverShuttingDown is returned when the resolver stops
|
||||||
// progressing because it received the quit signal.
|
// progressing because it received the quit signal.
|
||||||
|
@ -293,7 +293,7 @@ func (h *htlcIncomingContestResolver) report() *ContractReport {
|
|||||||
|
|
||||||
return &ContractReport{
|
return &ContractReport{
|
||||||
Outpoint: h.htlcResolution.ClaimOutpoint,
|
Outpoint: h.htlcResolution.ClaimOutpoint,
|
||||||
Incoming: true,
|
Type: ReportOutputIncomingHtlc,
|
||||||
Amount: finalAmt,
|
Amount: finalAmt,
|
||||||
MaturityHeight: h.htlcExpiry,
|
MaturityHeight: h.htlcExpiry,
|
||||||
LimboBalance: finalAmt,
|
LimboBalance: finalAmt,
|
||||||
|
@ -294,7 +294,7 @@ func (i *incomingResolverTestContext) waitForResult(expectSuccessRes bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !expectSuccessRes {
|
if !expectSuccessRes {
|
||||||
if err != nil {
|
if i.nextResolver != nil {
|
||||||
i.t.Fatal("expected no next resolver")
|
i.t.Fatal("expected no next resolver")
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
@ -166,7 +166,7 @@ func (h *htlcOutgoingContestResolver) report() *ContractReport {
|
|||||||
|
|
||||||
return &ContractReport{
|
return &ContractReport{
|
||||||
Outpoint: h.htlcResolution.ClaimOutpoint,
|
Outpoint: h.htlcResolution.ClaimOutpoint,
|
||||||
Incoming: false,
|
Type: ReportOutputOutgoingHtlc,
|
||||||
Amount: finalAmt,
|
Amount: finalAmt,
|
||||||
MaturityHeight: h.htlcResolution.Expiry,
|
MaturityHeight: h.htlcResolution.Expiry,
|
||||||
LimboBalance: finalAmt,
|
LimboBalance: finalAmt,
|
||||||
|
@ -211,7 +211,7 @@ func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) {
|
|||||||
h, h.htlc.RHash[:])
|
h, h.htlc.RHash[:])
|
||||||
|
|
||||||
err := h.IncubateOutputs(
|
err := h.IncubateOutputs(
|
||||||
h.ChanPoint, nil, nil, &h.htlcResolution,
|
h.ChanPoint, nil, &h.htlcResolution,
|
||||||
h.broadcastHeight,
|
h.broadcastHeight,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -247,7 +247,7 @@ func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) {
|
|||||||
h.htlcResolution.ClaimOutpoint)
|
h.htlcResolution.ClaimOutpoint)
|
||||||
|
|
||||||
err := h.IncubateOutputs(
|
err := h.IncubateOutputs(
|
||||||
h.ChanPoint, nil, &h.htlcResolution, nil,
|
h.ChanPoint, &h.htlcResolution, nil,
|
||||||
h.broadcastHeight,
|
h.broadcastHeight,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -216,7 +216,6 @@ func TestHtlcTimeoutResolver(t *testing.T) {
|
|||||||
Notifier: notifier,
|
Notifier: notifier,
|
||||||
PreimageDB: witnessBeacon,
|
PreimageDB: witnessBeacon,
|
||||||
IncubateOutputs: func(wire.OutPoint,
|
IncubateOutputs: func(wire.OutPoint,
|
||||||
*lnwallet.CommitOutputResolution,
|
|
||||||
*lnwallet.OutgoingHtlcResolution,
|
*lnwallet.OutgoingHtlcResolution,
|
||||||
*lnwallet.IncomingHtlcResolution,
|
*lnwallet.IncomingHtlcResolution,
|
||||||
uint32) error {
|
uint32) error {
|
||||||
|
@ -3,11 +3,14 @@ package contractcourt
|
|||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
"github.com/lightningnetwork/lnd/htlcswitch/hop"
|
"github.com/lightningnetwork/lnd/htlcswitch/hop"
|
||||||
|
"github.com/lightningnetwork/lnd/input"
|
||||||
"github.com/lightningnetwork/lnd/invoices"
|
"github.com/lightningnetwork/lnd/invoices"
|
||||||
"github.com/lightningnetwork/lnd/lntypes"
|
"github.com/lightningnetwork/lnd/lntypes"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
"github.com/lightningnetwork/lnd/sweep"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Registry is an interface which represents the invoice registry.
|
// Registry is an interface which represents the invoice registry.
|
||||||
@ -36,3 +39,16 @@ type OnionProcessor interface {
|
|||||||
// the passed io.Reader instance.
|
// the passed io.Reader instance.
|
||||||
ReconstructHopIterator(r io.Reader, rHash []byte) (hop.Iterator, error)
|
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 {
|
type inputKit struct {
|
||||||
outpoint wire.OutPoint
|
outpoint wire.OutPoint
|
||||||
witnessType WitnessType
|
witnessType WitnessType
|
||||||
signDesc SignDescriptor
|
signDesc SignDescriptor
|
||||||
heightHint uint32
|
heightHint uint32
|
||||||
|
blockToMaturity uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
// OutPoint returns the breached output's identifier that is to be included as
|
// 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
|
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
|
// BaseInput contains all the information needed to sweep a basic output
|
||||||
// (CSV/CLTV/no time lock)
|
// (CSV/CLTV/no time lock)
|
||||||
type BaseInput struct {
|
type BaseInput struct {
|
||||||
@ -107,6 +115,23 @@ func NewBaseInput(outpoint *wire.OutPoint, witnessType WitnessType,
|
|||||||
return &input
|
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
|
// CraftInputScript returns a valid set of input scripts allowing this output
|
||||||
// to be spent. The returned input scripts should target the input at location
|
// to be spent. The returned input scripts should target the input at location
|
||||||
// txIndex within the passed transaction. The input scripts generated by this
|
// 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)
|
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
|
// 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
|
// is expected to reside on the commitment tx of the remote party and should
|
||||||
// not be a second level tx output.
|
// not be a second level tx output.
|
||||||
@ -175,13 +193,6 @@ func (h *HtlcSucceedInput) CraftInputScript(signer Signer, txn *wire.MsgTx,
|
|||||||
}, nil
|
}, 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
|
// Compile-time constraints to ensure each input struct implement the Input
|
||||||
// interface.
|
// interface.
|
||||||
var _ Input = (*BaseInput)(nil)
|
var _ Input = (*BaseInput)(nil)
|
||||||
|
60
rpcserver.go
60
rpcserver.go
@ -34,6 +34,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/chanbackup"
|
"github.com/lightningnetwork/lnd/chanbackup"
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
"github.com/lightningnetwork/lnd/channelnotifier"
|
"github.com/lightningnetwork/lnd/channelnotifier"
|
||||||
|
"github.com/lightningnetwork/lnd/contractcourt"
|
||||||
"github.com/lightningnetwork/lnd/discovery"
|
"github.com/lightningnetwork/lnd/discovery"
|
||||||
"github.com/lightningnetwork/lnd/htlcswitch"
|
"github.com/lightningnetwork/lnd/htlcswitch"
|
||||||
"github.com/lightningnetwork/lnd/input"
|
"github.com/lightningnetwork/lnd/input"
|
||||||
@ -2451,23 +2452,51 @@ func (r *rpcServer) arbitratorPopulateForceCloseResp(chanPoint *wire.OutPoint,
|
|||||||
reports := arbitrator.Report()
|
reports := arbitrator.Report()
|
||||||
|
|
||||||
for _, report := range reports {
|
for _, report := range reports {
|
||||||
htlc := &lnrpc.PendingHTLC{
|
switch report.Type {
|
||||||
Incoming: report.Incoming,
|
|
||||||
Amount: int64(report.Amount),
|
|
||||||
Outpoint: report.Outpoint.String(),
|
|
||||||
MaturityHeight: report.MaturityHeight,
|
|
||||||
Stage: report.Stage,
|
|
||||||
}
|
|
||||||
|
|
||||||
if htlc.MaturityHeight != 0 {
|
// For a direct output, populate/update the top level
|
||||||
htlc.BlocksTilMaturity =
|
// response properties.
|
||||||
int32(htlc.MaturityHeight) - currentHeight
|
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.LimboBalance += int64(report.LimboBalance)
|
||||||
forceClose.RecoveredBalance += int64(report.RecoveredBalance)
|
forceClose.RecoveredBalance += int64(report.RecoveredBalance)
|
||||||
|
|
||||||
forceClose.PendingHtlcs = append(forceClose.PendingHtlcs, htlc)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -2498,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
|
||||||
|
@ -847,7 +847,6 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB,
|
|||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
IncubateOutputs: func(chanPoint wire.OutPoint,
|
IncubateOutputs: func(chanPoint wire.OutPoint,
|
||||||
commitRes *lnwallet.CommitOutputResolution,
|
|
||||||
outHtlcRes *lnwallet.OutgoingHtlcResolution,
|
outHtlcRes *lnwallet.OutgoingHtlcResolution,
|
||||||
inHtlcRes *lnwallet.IncomingHtlcResolution,
|
inHtlcRes *lnwallet.IncomingHtlcResolution,
|
||||||
broadcastHeight uint32) error {
|
broadcastHeight uint32) error {
|
||||||
@ -864,7 +863,7 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB,
|
|||||||
}
|
}
|
||||||
|
|
||||||
return s.utxoNursery.IncubateOutputs(
|
return s.utxoNursery.IncubateOutputs(
|
||||||
chanPoint, commitRes, outRes, inRes,
|
chanPoint, outRes, inRes,
|
||||||
broadcastHeight,
|
broadcastHeight,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
@ -330,7 +330,6 @@ func (u *utxoNursery) Stop() error {
|
|||||||
// they're CLTV absolute time locked, or if they're CSV relative time locked.
|
// 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.
|
// Once all outputs reach maturity, they'll be swept back into the wallet.
|
||||||
func (u *utxoNursery) IncubateOutputs(chanPoint wire.OutPoint,
|
func (u *utxoNursery) IncubateOutputs(chanPoint wire.OutPoint,
|
||||||
commitResolution *lnwallet.CommitOutputResolution,
|
|
||||||
outgoingHtlcs []lnwallet.OutgoingHtlcResolution,
|
outgoingHtlcs []lnwallet.OutgoingHtlcResolution,
|
||||||
incomingHtlcs []lnwallet.IncomingHtlcResolution,
|
incomingHtlcs []lnwallet.IncomingHtlcResolution,
|
||||||
broadcastHeight uint32) error {
|
broadcastHeight uint32) error {
|
||||||
@ -352,8 +351,6 @@ func (u *utxoNursery) IncubateOutputs(chanPoint wire.OutPoint,
|
|||||||
|
|
||||||
numHtlcs := len(incomingHtlcs) + len(outgoingHtlcs)
|
numHtlcs := len(incomingHtlcs) + len(outgoingHtlcs)
|
||||||
var (
|
var (
|
||||||
hasCommit bool
|
|
||||||
|
|
||||||
// Kid outputs can be swept after an initial confirmation
|
// Kid outputs can be swept after an initial confirmation
|
||||||
// followed by a maturity period.Baby outputs are two stage and
|
// followed by a maturity period.Baby outputs are two stage and
|
||||||
// will need to wait for an absolute time out to reach a
|
// 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.
|
// 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?
|
// 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
|
// 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
|
// * need ability to cancel in the case that we learn of pre-image or
|
||||||
// remote party pulls
|
// remote party pulls
|
||||||
|
|
||||||
utxnLog.Infof("Incubating Channel(%s) has-commit=%v, num-htlcs=%d",
|
utxnLog.Infof("Incubating Channel(%s) num-htlcs=%d",
|
||||||
chanPoint, hasCommit, numHtlcs)
|
chanPoint, numHtlcs)
|
||||||
|
|
||||||
u.mu.Lock()
|
u.mu.Lock()
|
||||||
defer u.mu.Unlock()
|
defer u.mu.Unlock()
|
||||||
@ -538,8 +513,6 @@ func (u *utxoNursery) NurseryReport(
|
|||||||
// Preschool outputs are awaiting the
|
// Preschool outputs are awaiting the
|
||||||
// confirmation of the commitment transaction.
|
// confirmation of the commitment transaction.
|
||||||
switch kid.WitnessType() {
|
switch kid.WitnessType() {
|
||||||
case input.CommitmentTimeLock:
|
|
||||||
report.AddLimboCommitment(&kid)
|
|
||||||
|
|
||||||
case input.HtlcAcceptedSuccessSecondLevel:
|
case input.HtlcAcceptedSuccessSecondLevel:
|
||||||
// An HTLC output on our commitment transaction
|
// An HTLC output on our commitment transaction
|
||||||
@ -561,11 +534,6 @@ func (u *utxoNursery) NurseryReport(
|
|||||||
// We can distinguish them via their witness
|
// We can distinguish them via their witness
|
||||||
// types.
|
// types.
|
||||||
switch kid.WitnessType() {
|
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:
|
case input.HtlcOfferedRemoteTimeout:
|
||||||
// This is an HTLC output on the
|
// This is an HTLC output on the
|
||||||
@ -590,11 +558,6 @@ func (u *utxoNursery) NurseryReport(
|
|||||||
// will contribute towards the recovered
|
// will contribute towards the recovered
|
||||||
// balance.
|
// balance.
|
||||||
switch kid.WitnessType() {
|
switch kid.WitnessType() {
|
||||||
case input.CommitmentTimeLock:
|
|
||||||
// The commitment output was
|
|
||||||
// successfully swept back into a
|
|
||||||
// regular p2wkh output.
|
|
||||||
report.AddRecoveredCommitment(&kid)
|
|
||||||
|
|
||||||
case input.HtlcAcceptedSuccessSecondLevel:
|
case input.HtlcAcceptedSuccessSecondLevel:
|
||||||
fallthrough
|
fallthrough
|
||||||
@ -1071,11 +1034,6 @@ type contractMaturityReport struct {
|
|||||||
// recoveredBalance is the total value that has been successfully swept
|
// recoveredBalance is the total value that has been successfully swept
|
||||||
// back to the user's wallet.
|
// back to the user's wallet.
|
||||||
recoveredBalance btcutil.Amount
|
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 records a maturity report for each htlc output in this channel.
|
||||||
htlcs []htlcMaturityReport
|
htlcs []htlcMaturityReport
|
||||||
}
|
}
|
||||||
@ -1100,26 +1058,6 @@ type htlcMaturityReport struct {
|
|||||||
stage uint32
|
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
|
// AddLimboStage1TimeoutHtlc adds an htlc crib output to the maturity report's
|
||||||
// htlcs, and contributes its amount to the limbo balance.
|
// htlcs, and contributes its amount to the limbo balance.
|
||||||
func (c *contractMaturityReport) AddLimboStage1TimeoutHtlc(baby *babyOutput) {
|
func (c *contractMaturityReport) AddLimboStage1TimeoutHtlc(baby *babyOutput) {
|
||||||
|
@ -650,7 +650,6 @@ func incubateTestOutput(t *testing.T, nursery *utxoNursery,
|
|||||||
// Hand off to nursery.
|
// Hand off to nursery.
|
||||||
err := nursery.IncubateOutputs(
|
err := nursery.IncubateOutputs(
|
||||||
testChanPoint,
|
testChanPoint,
|
||||||
nil,
|
|
||||||
[]lnwallet.OutgoingHtlcResolution{*outgoingRes},
|
[]lnwallet.OutgoingHtlcResolution{*outgoingRes},
|
||||||
nil, 0,
|
nil, 0,
|
||||||
)
|
)
|
||||||
@ -839,59 +838,6 @@ func testNurseryOutgoingHtlcSuccessOnRemote(t *testing.T,
|
|||||||
ctx.finish()
|
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) {
|
func testSweepHtlc(t *testing.T, ctx *nurseryTestContext) {
|
||||||
testSweep(t, ctx, func() {
|
testSweep(t, ctx, func() {
|
||||||
// Verify stage in nursery report. HTLCs should now both still
|
// Verify stage in nursery report. HTLCs should now both still
|
||||||
|
Loading…
Reference in New Issue
Block a user