mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-01-19 05:45:21 +01:00
lnwallet: add ability to add extra co-op close outputs
This commit is contained in:
parent
7b396f4969
commit
517608ca3b
@ -7691,10 +7691,21 @@ func NewLocalForceCloseSummary(chanState *channeldb.OpenChannel,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CloseOutput wraps a normal tx out with additional metadata that indicates if
|
||||
// the output belongs to the initiator of the channel or not.
|
||||
type CloseOutput struct {
|
||||
wire.TxOut
|
||||
|
||||
// IsLocal indicates if the output belong to the local party.
|
||||
IsLocal bool
|
||||
}
|
||||
|
||||
// chanCloseOpt is a functional option that can be used to modify the co-op
|
||||
// close process.
|
||||
type chanCloseOpt struct {
|
||||
musigSession *MusigSession
|
||||
|
||||
extraCloseOutputs []CloseOutput
|
||||
}
|
||||
|
||||
// ChanCloseOpt is a closure type that cen be used to modify the set of default
|
||||
@ -7715,6 +7726,14 @@ func WithCoopCloseMusigSession(session *MusigSession) ChanCloseOpt {
|
||||
}
|
||||
}
|
||||
|
||||
// WithExtraCloseOutputs can be used to add extra outputs to the cooperative
|
||||
// transaction.
|
||||
func WithExtraCloseOutputs(extraOutputs []CloseOutput) ChanCloseOpt {
|
||||
return func(opts *chanCloseOpt) {
|
||||
opts.extraCloseOutputs = extraOutputs
|
||||
}
|
||||
}
|
||||
|
||||
// CreateCloseProposal is used by both parties in a cooperative channel close
|
||||
// workflow to generate proposed close transactions and signatures. This method
|
||||
// should only be executed once all pending HTLCs (if any) on the channel have
|
||||
@ -7722,9 +7741,6 @@ func WithCoopCloseMusigSession(session *MusigSession) ChanCloseOpt {
|
||||
// the "closing" state, which indicates that all incoming/outgoing HTLC
|
||||
// requests should be rejected. A signature for the closing transaction is
|
||||
// returned.
|
||||
//
|
||||
// TODO(roasbeef): caller should initiate signal to reject all incoming HTLCs,
|
||||
// settle any in flight.
|
||||
func (lc *LightningChannel) CreateCloseProposal(proposedFee btcutil.Amount,
|
||||
localDeliveryScript []byte, remoteDeliveryScript []byte,
|
||||
closeOpts ...ChanCloseOpt) (input.Signature, *chainhash.Hash,
|
||||
@ -7735,7 +7751,6 @@ func (lc *LightningChannel) CreateCloseProposal(proposedFee btcutil.Amount,
|
||||
|
||||
// If we're already closing the channel, then ignore this request.
|
||||
if lc.isClosed {
|
||||
// TODO(roasbeef): check to ensure no pending payments
|
||||
return nil, nil, 0, ErrChanClosing
|
||||
}
|
||||
|
||||
@ -7762,6 +7777,14 @@ func (lc *LightningChannel) CreateCloseProposal(proposedFee btcutil.Amount,
|
||||
closeTxOpts = append(closeTxOpts, WithRBFCloseTx())
|
||||
}
|
||||
|
||||
// If we have any extra outputs to pass along, then we'll map that to
|
||||
// the co-op close option txn type.
|
||||
if opts.extraCloseOutputs != nil {
|
||||
closeTxOpts = append(closeTxOpts, WithExtraTxCloseOutputs(
|
||||
opts.extraCloseOutputs,
|
||||
))
|
||||
}
|
||||
|
||||
closeTx := CreateCooperativeCloseTx(
|
||||
fundingTxIn(lc.channelState), lc.channelState.LocalChanCfg.DustLimit,
|
||||
lc.channelState.RemoteChanCfg.DustLimit, ourBalance, theirBalance,
|
||||
@ -7844,6 +7867,14 @@ func (lc *LightningChannel) CompleteCooperativeClose(
|
||||
closeTxOpts = append(closeTxOpts, WithRBFCloseTx())
|
||||
}
|
||||
|
||||
// If we have any extra outputs to pass along, then we'll map that to
|
||||
// the co-op close option txn type.
|
||||
if opts.extraCloseOutputs != nil {
|
||||
closeTxOpts = append(closeTxOpts, WithExtraTxCloseOutputs(
|
||||
opts.extraCloseOutputs,
|
||||
))
|
||||
}
|
||||
|
||||
// Create the transaction used to return the current settled balance
|
||||
// on this active channel back to both parties. In this current model,
|
||||
// the initiator pays full fees for the cooperative close transaction.
|
||||
@ -8549,6 +8580,10 @@ type closeTxOpts struct {
|
||||
// enableRBF indicates whether the cooperative close tx should signal
|
||||
// RBF or not.
|
||||
enableRBF bool
|
||||
|
||||
// extraCloseOutputs is a set of additional outputs that should be
|
||||
// added the co-op close transaction.
|
||||
extraCloseOutputs []CloseOutput
|
||||
}
|
||||
|
||||
// defaultCloseTxOpts returns a closeTxOpts struct with default values.
|
||||
@ -8569,6 +8604,14 @@ func WithRBFCloseTx() CloseTxOpt {
|
||||
}
|
||||
}
|
||||
|
||||
// WithExtraTxCloseOutputs can be used to add extra outputs to the cooperative
|
||||
// transaction.
|
||||
func WithExtraTxCloseOutputs(extraOutputs []CloseOutput) CloseTxOpt {
|
||||
return func(o *closeTxOpts) {
|
||||
o.extraCloseOutputs = extraOutputs
|
||||
}
|
||||
}
|
||||
|
||||
// CreateCooperativeCloseTx creates a transaction which if signed by both
|
||||
// parties, then broadcast cooperatively closes an active channel. The creation
|
||||
// of the closure transaction is modified by a boolean indicating if the party
|
||||
@ -8600,17 +8643,115 @@ func CreateCooperativeCloseTx(fundingTxIn wire.TxIn,
|
||||
|
||||
// Create both cooperative closure outputs, properly respecting the
|
||||
// dust limits of both parties.
|
||||
if ourBalance >= localDust {
|
||||
var localOutputIdx fn.Option[int]
|
||||
haveLocalOutput := ourBalance >= localDust
|
||||
if haveLocalOutput {
|
||||
closeTx.AddTxOut(&wire.TxOut{
|
||||
PkScript: ourDeliveryScript,
|
||||
Value: int64(ourBalance),
|
||||
})
|
||||
|
||||
localOutputIdx = fn.Some(len(closeTx.TxOut) - 1)
|
||||
}
|
||||
if theirBalance >= remoteDust {
|
||||
|
||||
var remoteOutputIdx fn.Option[int]
|
||||
haveRemoteOutput := theirBalance >= remoteDust
|
||||
if haveRemoteOutput {
|
||||
closeTx.AddTxOut(&wire.TxOut{
|
||||
PkScript: theirDeliveryScript,
|
||||
Value: int64(theirBalance),
|
||||
})
|
||||
|
||||
remoteOutputIdx = fn.Some(len(closeTx.TxOut) - 1)
|
||||
}
|
||||
|
||||
// If we have extra outputs to add to the co-op close transaction, then
|
||||
// we'll examine them now. We'll deduct the output's value from the
|
||||
// owning party. In the case that a party can't pay for the output, then
|
||||
// their normal output will be omitted.
|
||||
for _, extraTxOut := range opts.extraCloseOutputs {
|
||||
switch {
|
||||
// For additional local outputs, add the output, then deduct
|
||||
// the balance from our local balance.
|
||||
case extraTxOut.IsLocal:
|
||||
// The extraCloseOutputs in the options just indicate if
|
||||
// an extra output should be added in general. But we
|
||||
// only add one if we actually _need_ one, based on the
|
||||
// balance. If we don't have enough local balance to
|
||||
// cover the extra output, then localOutputIdx is None.
|
||||
localOutputIdx.WhenSome(func(idx int) {
|
||||
// The output that currently represents the
|
||||
// local balance, which means:
|
||||
// txOut.Value == ourBalance.
|
||||
txOut := closeTx.TxOut[idx]
|
||||
|
||||
// The extra output (if one exists) is the more
|
||||
// important one, as in custom channels it might
|
||||
// carry some additional values. The normal
|
||||
// output is just an address that sends the
|
||||
// local balance back to our wallet. The extra
|
||||
// one also goes to our wallet, but might also
|
||||
// carry other values, so it has higher
|
||||
// priority. Do we have enough balance to have
|
||||
// both the extra output with the given value
|
||||
// (which is subtracted from our balance) and
|
||||
// still an above-dust normal output? If not, we
|
||||
// skip the extra output and just overwrite the
|
||||
// existing output script with the one from the
|
||||
// extra output.
|
||||
amtAfterOutput := btcutil.Amount(
|
||||
txOut.Value - extraTxOut.Value,
|
||||
)
|
||||
if amtAfterOutput <= localDust {
|
||||
txOut.PkScript = extraTxOut.PkScript
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
txOut.Value -= extraTxOut.Value
|
||||
closeTx.AddTxOut(&extraTxOut.TxOut)
|
||||
})
|
||||
|
||||
// For extra remote outputs, we'll do the opposite.
|
||||
case !extraTxOut.IsLocal:
|
||||
// The extraCloseOutputs in the options just indicate if
|
||||
// an extra output should be added in general. But we
|
||||
// only add one if we actually _need_ one, based on the
|
||||
// balance. If we don't have enough remote balance to
|
||||
// cover the extra output, then remoteOutputIdx is None.
|
||||
remoteOutputIdx.WhenSome(func(idx int) {
|
||||
// The output that currently represents the
|
||||
// remote balance, which means:
|
||||
// txOut.Value == theirBalance.
|
||||
txOut := closeTx.TxOut[idx]
|
||||
|
||||
// The extra output (if one exists) is the more
|
||||
// important one, as in custom channels it might
|
||||
// carry some additional values. The normal
|
||||
// output is just an address that sends the
|
||||
// remote balance back to their wallet. The
|
||||
// extra one also goes to their wallet, but
|
||||
// might also carry other values, so it has
|
||||
// higher priority. Do they have enough balance
|
||||
// to have both the extra output with the given
|
||||
// value (which is subtracted from their
|
||||
// balance) and still an above-dust normal
|
||||
// output? If not, we skip the extra output and
|
||||
// just overwrite the existing output script
|
||||
// with the one from the extra output.
|
||||
amtAfterOutput := btcutil.Amount(
|
||||
txOut.Value - extraTxOut.Value,
|
||||
)
|
||||
if amtAfterOutput <= remoteDust {
|
||||
txOut.PkScript = extraTxOut.PkScript
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
txOut.Value -= extraTxOut.Value
|
||||
closeTx.AddTxOut(&extraTxOut.TxOut)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
txsort.InPlaceSort(closeTx)
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/btcutil/txsort"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
@ -11476,3 +11477,315 @@ func TestBlindingPointPersistence(t *testing.T) {
|
||||
require.Equal(t, blinding,
|
||||
bobCommit.incomingHTLCs[0].BlindingPoint.UnwrapOrFailV(t))
|
||||
}
|
||||
|
||||
// TestCreateCooperativeCloseTx tests that the cooperative close transaction is
|
||||
// properly created based on the standard and also optional parameters.
|
||||
func TestCreateCooperativeCloseTx(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fundingTxIn := &wire.TxIn{}
|
||||
|
||||
localDust := btcutil.Amount(400)
|
||||
remoteDust := btcutil.Amount(400)
|
||||
|
||||
localScript := []byte{0}
|
||||
localExtraScript := []byte{2}
|
||||
|
||||
remoteScript := []byte{1}
|
||||
remoteExtraScript := []byte{3}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
enableRBF bool
|
||||
|
||||
localBalance btcutil.Amount
|
||||
remoteBalance btcutil.Amount
|
||||
|
||||
extraCloseOutputs []CloseOutput
|
||||
|
||||
expectedTx *wire.MsgTx
|
||||
}{
|
||||
{
|
||||
name: "no dust, no extra outputs",
|
||||
localBalance: 1_000,
|
||||
remoteBalance: 1_000,
|
||||
expectedTx: &wire.MsgTx{
|
||||
TxIn: []*wire.TxIn{
|
||||
fundingTxIn,
|
||||
},
|
||||
Version: 2,
|
||||
TxOut: []*wire.TxOut{
|
||||
{
|
||||
Value: 1_000,
|
||||
PkScript: localScript,
|
||||
},
|
||||
{
|
||||
Value: 1_000,
|
||||
PkScript: remoteScript,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "local dust, no extra outputs",
|
||||
localBalance: 100,
|
||||
remoteBalance: 1_000,
|
||||
expectedTx: &wire.MsgTx{
|
||||
TxIn: []*wire.TxIn{
|
||||
fundingTxIn,
|
||||
},
|
||||
Version: 2,
|
||||
TxOut: []*wire.TxOut{
|
||||
{
|
||||
Value: 1_000,
|
||||
PkScript: remoteScript,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remote dust, no extra outputs",
|
||||
localBalance: 1_000,
|
||||
remoteBalance: 100,
|
||||
expectedTx: &wire.MsgTx{
|
||||
TxIn: []*wire.TxIn{
|
||||
fundingTxIn,
|
||||
},
|
||||
Version: 2,
|
||||
TxOut: []*wire.TxOut{
|
||||
{
|
||||
Value: 1_000,
|
||||
PkScript: localScript,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no dust, local extra output",
|
||||
localBalance: 10_000,
|
||||
remoteBalance: 10_000,
|
||||
extraCloseOutputs: []CloseOutput{
|
||||
{
|
||||
TxOut: wire.TxOut{
|
||||
Value: 1_000,
|
||||
PkScript: localExtraScript,
|
||||
},
|
||||
IsLocal: true,
|
||||
},
|
||||
},
|
||||
expectedTx: &wire.MsgTx{
|
||||
TxIn: []*wire.TxIn{
|
||||
fundingTxIn,
|
||||
},
|
||||
Version: 2,
|
||||
TxOut: []*wire.TxOut{
|
||||
{
|
||||
Value: 10_000,
|
||||
PkScript: remoteScript,
|
||||
},
|
||||
{
|
||||
Value: 9_000,
|
||||
PkScript: localScript,
|
||||
},
|
||||
{
|
||||
Value: 1_000,
|
||||
PkScript: localExtraScript,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no dust, remote extra output",
|
||||
localBalance: 10_000,
|
||||
remoteBalance: 10_000,
|
||||
extraCloseOutputs: []CloseOutput{
|
||||
{
|
||||
TxOut: wire.TxOut{
|
||||
Value: 1_000,
|
||||
PkScript: remoteExtraScript,
|
||||
},
|
||||
IsLocal: false,
|
||||
},
|
||||
},
|
||||
expectedTx: &wire.MsgTx{
|
||||
TxIn: []*wire.TxIn{
|
||||
fundingTxIn,
|
||||
},
|
||||
Version: 2,
|
||||
TxOut: []*wire.TxOut{
|
||||
{
|
||||
Value: 10_000,
|
||||
PkScript: localScript,
|
||||
},
|
||||
{
|
||||
Value: 9_000,
|
||||
PkScript: remoteScript,
|
||||
},
|
||||
{
|
||||
Value: 1_000,
|
||||
PkScript: remoteExtraScript,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no dust, local+remote extra output",
|
||||
localBalance: 10_000,
|
||||
remoteBalance: 10_000,
|
||||
extraCloseOutputs: []CloseOutput{
|
||||
{
|
||||
TxOut: wire.TxOut{
|
||||
Value: 1_000,
|
||||
PkScript: remoteExtraScript,
|
||||
},
|
||||
IsLocal: false,
|
||||
},
|
||||
{
|
||||
TxOut: wire.TxOut{
|
||||
Value: 1_000,
|
||||
PkScript: localExtraScript,
|
||||
},
|
||||
IsLocal: true,
|
||||
},
|
||||
},
|
||||
expectedTx: &wire.MsgTx{
|
||||
TxIn: []*wire.TxIn{
|
||||
fundingTxIn,
|
||||
},
|
||||
Version: 2,
|
||||
TxOut: []*wire.TxOut{
|
||||
{
|
||||
Value: 9_000,
|
||||
PkScript: localScript,
|
||||
},
|
||||
{
|
||||
Value: 9_000,
|
||||
PkScript: remoteScript,
|
||||
},
|
||||
{
|
||||
Value: 1_000,
|
||||
PkScript: remoteExtraScript,
|
||||
},
|
||||
{
|
||||
Value: 1_000,
|
||||
PkScript: localExtraScript,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no dust, local+remote extra output, " +
|
||||
"remote can't afford",
|
||||
localBalance: 10_000,
|
||||
remoteBalance: 1_000,
|
||||
extraCloseOutputs: []CloseOutput{
|
||||
{
|
||||
TxOut: wire.TxOut{
|
||||
Value: 1_000,
|
||||
PkScript: remoteExtraScript,
|
||||
},
|
||||
IsLocal: false,
|
||||
},
|
||||
{
|
||||
TxOut: wire.TxOut{
|
||||
Value: 1_000,
|
||||
PkScript: localExtraScript,
|
||||
},
|
||||
IsLocal: true,
|
||||
},
|
||||
},
|
||||
expectedTx: &wire.MsgTx{
|
||||
TxIn: []*wire.TxIn{
|
||||
fundingTxIn,
|
||||
},
|
||||
Version: 2,
|
||||
TxOut: []*wire.TxOut{
|
||||
{
|
||||
Value: 9_000,
|
||||
PkScript: localScript,
|
||||
},
|
||||
{
|
||||
Value: 1_000,
|
||||
PkScript: remoteExtraScript,
|
||||
},
|
||||
{
|
||||
Value: 1_000,
|
||||
PkScript: localExtraScript,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no dust, local+remote extra output, " +
|
||||
"local can't afford",
|
||||
localBalance: 1_000,
|
||||
remoteBalance: 10_000,
|
||||
extraCloseOutputs: []CloseOutput{
|
||||
{
|
||||
TxOut: wire.TxOut{
|
||||
Value: 1_000,
|
||||
PkScript: remoteExtraScript,
|
||||
},
|
||||
IsLocal: false,
|
||||
},
|
||||
{
|
||||
TxOut: wire.TxOut{
|
||||
Value: 1_000,
|
||||
PkScript: localExtraScript,
|
||||
},
|
||||
IsLocal: true,
|
||||
},
|
||||
},
|
||||
expectedTx: &wire.MsgTx{
|
||||
TxIn: []*wire.TxIn{
|
||||
fundingTxIn,
|
||||
},
|
||||
Version: 2,
|
||||
TxOut: []*wire.TxOut{
|
||||
{
|
||||
Value: 9_000,
|
||||
PkScript: remoteScript,
|
||||
},
|
||||
{
|
||||
Value: 1_000,
|
||||
PkScript: remoteExtraScript,
|
||||
},
|
||||
{
|
||||
Value: 1_000,
|
||||
PkScript: localExtraScript,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var opts []CloseTxOpt
|
||||
if test.extraCloseOutputs != nil {
|
||||
opts = append(
|
||||
opts,
|
||||
WithExtraTxCloseOutputs(
|
||||
test.extraCloseOutputs,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
closeTx := CreateCooperativeCloseTx(
|
||||
*fundingTxIn, localDust, remoteDust,
|
||||
test.localBalance, test.remoteBalance,
|
||||
localScript, remoteScript, opts...,
|
||||
)
|
||||
|
||||
txsort.InPlaceSort(test.expectedTx)
|
||||
|
||||
require.Equal(
|
||||
t, test.expectedTx, closeTx,
|
||||
"expected %v, got %v",
|
||||
spew.Sdump(test.expectedTx),
|
||||
spew.Sdump(closeTx),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user