lnwallet: implement special case for OP_RETURN in rbf-coop

In this commit, we implement a special case for OP_RETURN scripts
outlined in the spec. If a party decides that its output will be too
small even after the dust check, then they can opt to set it to zero by sending an `OP_RETURN` as their script.
This commit is contained in:
Olaoluwa Osuntokun 2025-02-07 18:22:15 -08:00
parent e6d7a1a2ec
commit 333492e657
2 changed files with 85 additions and 4 deletions

View File

@ -9269,13 +9269,19 @@ func CreateCooperativeCloseTx(fundingTxIn wire.TxIn,
closeTx.LockTime = lockTime
})
// TODO(roasbeef): needs support for dropping inputs
// Create both cooperative closure outputs, properly respecting the
// dust limits of both parties.
// Create both cooperative closure outputs, properly respecting the dust
// limits of both parties.
var localOutputIdx fn.Option[int]
haveLocalOutput := ourBalance >= localDust
if haveLocalOutput {
// If our script is an OP_RETURN, then we set our balance to
// zero.
if opts.customSequence.IsSome() &&
input.ScriptIsOpReturn(ourDeliveryScript) {
ourBalance = 0
}
closeTx.AddTxOut(&wire.TxOut{
PkScript: ourDeliveryScript,
Value: int64(ourBalance),
@ -9287,6 +9293,14 @@ func CreateCooperativeCloseTx(fundingTxIn wire.TxIn,
var remoteOutputIdx fn.Option[int]
haveRemoteOutput := theirBalance >= remoteDust
if haveRemoteOutput {
// If a party's script is an OP_RETURN, then we set their
// balance to zero.
if opts.customSequence.IsSome() &&
input.ScriptIsOpReturn(theirDeliveryScript) {
theirBalance = 0
}
closeTx.AddTxOut(&wire.TxOut{
PkScript: theirDeliveryScript,
Value: int64(theirBalance),

View File

@ -20,6 +20,7 @@ import (
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/txsort"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/mempool"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/davecgh/go-spew/spew"
@ -2450,6 +2451,72 @@ func TestCooperativeCloseDustAdherence(t *testing.T) {
}
}
// TestCooperativeCloseOpReturn tests that if either party's script is an
// OP_RETURN script, then we'll set their output value as zero on the closing
// transaction.
func TestCooperativeCloseOpReturn(t *testing.T) {
t.Parallel()
// Create a test channel which will be used for the duration of this
// unittest. The channel will be funded evenly with Alice having 5 BTC,
// and Bob having 5 BTC.
aliceChannel, bobChannel, err := CreateTestChannels(
t, channeldb.SingleFunderTweaklessBit,
)
require.NoError(t, err, "unable to create test channels")
// Alice will have a "normal" looking script, while Bob will have a
// script that's just an OP_RETURN.
aliceDeliveryScript := bobsPrivKey
bobDeliveryScript := []byte{txscript.OP_RETURN}
aliceFeeRate := chainfee.SatPerKWeight(
aliceChannel.channelState.LocalCommitment.FeePerKw,
)
aliceFee := aliceChannel.CalcFee(aliceFeeRate) + 1000
assertBobOpReturn := func(tx *wire.MsgTx) {
// We should still have two outputs on the commitment
// transaction, as Alice's is non-dust.
require.Len(t, tx.TxOut, 2)
// We should find that Bob's output has a zero value.
bobTxOut := fn.Filter(tx.TxOut, func(txOut *wire.TxOut) bool {
return bytes.Equal(txOut.PkScript, bobDeliveryScript)
})
require.Len(t, bobTxOut, 1)
require.True(t, bobTxOut[0].Value == 0)
}
// Next, we'll make a new co-op close proposal, initiated by Alice.
aliceSig, closeTxAlice, _, err := aliceChannel.CreateCloseProposal(
aliceFee, aliceDeliveryScript, bobDeliveryScript,
// We use a custom sequence as this rule only applies to the RBF
// coop channel type.
WithCustomSequence(mempool.MaxRBFSequence),
)
require.NoError(t, err, "unable to close channel")
assertBobOpReturn(closeTxAlice)
bobSig, _, _, err := bobChannel.CreateCloseProposal(
aliceFee, bobDeliveryScript, aliceDeliveryScript,
WithCustomSequence(mempool.MaxRBFSequence),
)
require.NoError(t, err, "unable to close channel")
// We should now be able to complete the cooperative channel closure,
// finding that the close tx still only has a single output.
closeTx, _, err := bobChannel.CompleteCooperativeClose(
bobSig, aliceSig, bobDeliveryScript, aliceDeliveryScript,
aliceFee, WithCustomSequence(mempool.MaxRBFSequence),
)
require.NoError(t, err, "unable to accept channel close")
assertBobOpReturn(closeTx)
}
// TestUpdateFeeAdjustments tests that the state machine is able to properly
// accept valid fee changes, as well as reject any invalid fee updates.
func TestUpdateFeeAdjustments(t *testing.T) {