lnwallet: add ability to add extra co-op close outputs

This commit is contained in:
Olaoluwa Osuntokun 2024-05-29 19:57:39 +02:00 committed by Oliver Gugger
parent 7b396f4969
commit 517608ca3b
No known key found for this signature in database
GPG Key ID: 8E4256593F177720
2 changed files with 460 additions and 6 deletions

View File

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

View File

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