Merge pull request #9610 from lightningnetwork/rbf-staging
multi: integrate rbf changes from staging branch
This commit is contained in:
47 files changed
+5834
-2727
No files matched your search
@@ -4104,6 +4104,34 @@ func (c *OpenChannel) AbsoluteThawHeight() (uint32, error) {
|
|||||||
return c.ThawHeight, nil
|
return c.ThawHeight, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeriveHeightHint derives the block height for the channel opening.
|
||||||
|
func (c *OpenChannel) DeriveHeightHint() uint32 {
|
||||||
|
// As a height hint, we'll try to use the opening height, but if the
|
||||||
|
// channel isn't yet open, then we'll use the height it was broadcast
|
||||||
|
// at. This may be an unconfirmed zero-conf channel.
|
||||||
|
heightHint := c.ShortChanID().BlockHeight
|
||||||
|
if heightHint == 0 {
|
||||||
|
heightHint = c.BroadcastHeight()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since no zero-conf state is stored in a channel backup, the below
|
||||||
|
// logic will not be triggered for restored, zero-conf channels. Set
|
||||||
|
// the height hint for zero-conf channels.
|
||||||
|
if c.IsZeroConf() {
|
||||||
|
if c.ZeroConfConfirmed() {
|
||||||
|
// If the zero-conf channel is confirmed, we'll use the
|
||||||
|
// confirmed SCID's block height.
|
||||||
|
heightHint = c.ZeroConfRealScid().BlockHeight
|
||||||
|
} else {
|
||||||
|
// The zero-conf channel is unconfirmed. We'll need to
|
||||||
|
// use the FundingBroadcastHeight.
|
||||||
|
heightHint = c.BroadcastHeight()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return heightHint
|
||||||
|
}
|
||||||
|
|
||||||
func putChannelCloseSummary(tx kvdb.RwTx, chanID []byte,
|
func putChannelCloseSummary(tx kvdb.RwTx, chanID []byte,
|
||||||
summary *ChannelCloseSummary, lastChanState *OpenChannel) error {
|
summary *ChannelCloseSummary, lastChanState *OpenChannel) error {
|
||||||
|
|
||||||
|
|||||||
@@ -281,7 +281,7 @@ func newChainWatcher(cfg chainWatcherConfig) (*chainWatcher, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get the channel opening block height.
|
// Get the channel opening block height.
|
||||||
heightHint := deriveHeightHint(chanState)
|
heightHint := chanState.DeriveHeightHint()
|
||||||
|
|
||||||
// We'll register for a notification to be dispatched if the funding
|
// We'll register for a notification to be dispatched if the funding
|
||||||
// output is spent.
|
// output is spent.
|
||||||
@@ -1328,34 +1328,6 @@ func deriveFundingPkScript(chanState *channeldb.OpenChannel) ([]byte, error) {
|
|||||||
return fundingPkScript, nil
|
return fundingPkScript, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// deriveHeightHint derives the block height for the channel opening.
|
|
||||||
func deriveHeightHint(chanState *channeldb.OpenChannel) uint32 {
|
|
||||||
// As a height hint, we'll try to use the opening height, but if the
|
|
||||||
// channel isn't yet open, then we'll use the height it was broadcast
|
|
||||||
// at. This may be an unconfirmed zero-conf channel.
|
|
||||||
heightHint := chanState.ShortChanID().BlockHeight
|
|
||||||
if heightHint == 0 {
|
|
||||||
heightHint = chanState.BroadcastHeight()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Since no zero-conf state is stored in a channel backup, the below
|
|
||||||
// logic will not be triggered for restored, zero-conf channels. Set
|
|
||||||
// the height hint for zero-conf channels.
|
|
||||||
if chanState.IsZeroConf() {
|
|
||||||
if chanState.ZeroConfConfirmed() {
|
|
||||||
// If the zero-conf channel is confirmed, we'll use the
|
|
||||||
// confirmed SCID's block height.
|
|
||||||
heightHint = chanState.ZeroConfRealScid().BlockHeight
|
|
||||||
} else {
|
|
||||||
// The zero-conf channel is unconfirmed. We'll need to
|
|
||||||
// use the FundingBroadcastHeight.
|
|
||||||
heightHint = chanState.BroadcastHeight()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return heightHint
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleCommitSpend takes a spending tx of the funding output and handles the
|
// handleCommitSpend takes a spending tx of the funding output and handles the
|
||||||
// channel close based on the closure type.
|
// channel close based on the closure type.
|
||||||
func (c *chainWatcher) handleCommitSpend(
|
func (c *chainWatcher) handleCommitSpend(
|
||||||
|
|||||||
@@ -93,6 +93,27 @@
|
|||||||
|
|
||||||
# New Features
|
# New Features
|
||||||
|
|
||||||
|
* Add support for [archiving channel backup](https://github.com/lightningnetwork/lnd/pull/9232)
|
||||||
|
in a designated folder which allows for easy referencing in the future. A new
|
||||||
|
config is added `disable-backup-archive`, with default set to false, to
|
||||||
|
determine if previous channel backups should be archived or not.
|
||||||
|
|
||||||
|
## Protocol Updates
|
||||||
|
|
||||||
|
* `lnd` now [supports the new RBF cooperative close
|
||||||
|
flow](https://github.com/lightningnetwork/lnd/pull/9610). Unlike the old flow,
|
||||||
|
this version now uses RBF to enable either side to increase their fee rate using
|
||||||
|
their _own_ channel funds. This removes the old "negotiation" logic that could
|
||||||
|
fail, with a version where either side can increase the fee on their coop close
|
||||||
|
transaction using their channel balance.
|
||||||
|
|
||||||
|
This new feature can be activated with a new config flag:
|
||||||
|
`--protocol.rbf-coop-close`.
|
||||||
|
|
||||||
|
With this new co-op close type, users can issue multiple `lncli closechannnel`
|
||||||
|
commands with increasing fee rates to use RBF to bump an existing signed co-op
|
||||||
|
close transaction.
|
||||||
|
|
||||||
* [Support](https://github.com/lightningnetwork/lnd/pull/8390) for
|
* [Support](https://github.com/lightningnetwork/lnd/pull/8390) for
|
||||||
[experimental endorsement](https://github.com/lightning/blips/pull/27)
|
[experimental endorsement](https://github.com/lightning/blips/pull/27)
|
||||||
signal relay was added. This signal has *no impact* on routing, and
|
signal relay was added. This signal has *no impact* on routing, and
|
||||||
@@ -106,10 +127,7 @@
|
|||||||
initial historical sync may be blocked due to a race condition in handling the
|
initial historical sync may be blocked due to a race condition in handling the
|
||||||
syncer's internal state.
|
syncer's internal state.
|
||||||
|
|
||||||
* Add support for [archiving channel backup](https://github.com/lightningnetwork/lnd/pull/9232)
|
|
||||||
in a designated folder which allows for easy referencing in the future. A new
|
|
||||||
config is added `disable-backup-archive`, with default set to false, to
|
|
||||||
determine if previous channel backups should be archived or not.
|
|
||||||
|
|
||||||
* [The max fee rate](https://github.com/lightningnetwork/lnd/pull/9491) is now
|
* [The max fee rate](https://github.com/lightningnetwork/lnd/pull/9491) is now
|
||||||
respected when a coop close is initiated. Before the max fee rate would only
|
respected when a coop close is initiated. Before the max fee rate would only
|
||||||
@@ -432,6 +450,7 @@ The underlying functionality between those two options remain the same.
|
|||||||
* Keagan McClelland
|
* Keagan McClelland
|
||||||
* Nishant Bansal
|
* Nishant Bansal
|
||||||
* Oliver Gugger
|
* Oliver Gugger
|
||||||
|
* Olaoluwa Osuntokun
|
||||||
* Pins
|
* Pins
|
||||||
* Viktor Tigerström
|
* Viktor Tigerström
|
||||||
* Yong Yu
|
* Yong Yu
|
||||||
|
|||||||
@@ -103,4 +103,8 @@ var defaultSetDesc = setDesc{
|
|||||||
lnwire.ExperimentalEndorsementOptional: {
|
lnwire.ExperimentalEndorsementOptional: {
|
||||||
SetNodeAnn: {}, // N
|
SetNodeAnn: {}, // N
|
||||||
},
|
},
|
||||||
|
lnwire.RbfCoopCloseOptionalStaging: {
|
||||||
|
SetInit: {}, // I
|
||||||
|
SetNodeAnn: {}, // N
|
||||||
|
},
|
||||||
}
|
}
|
||||||
+7
-1
@@ -73,6 +73,10 @@ type Config struct {
|
|||||||
// forwarding experimental endorsement.
|
// forwarding experimental endorsement.
|
||||||
NoExperimentalEndorsement bool
|
NoExperimentalEndorsement bool
|
||||||
|
|
||||||
|
// NoRbfCoopClose unsets any bits that signal support for using RBF for
|
||||||
|
// coop close.
|
||||||
|
NoRbfCoopClose bool
|
||||||
|
|
||||||
// CustomFeatures is a set of custom features to advertise in each
|
// CustomFeatures is a set of custom features to advertise in each
|
||||||
// set.
|
// set.
|
||||||
CustomFeatures map[Set][]lnwire.FeatureBit
|
CustomFeatures map[Set][]lnwire.FeatureBit
|
||||||
@@ -209,11 +213,13 @@ func newManager(cfg Config, desc setDesc) (*Manager, error) {
|
|||||||
raw.Unset(lnwire.SimpleTaprootOverlayChansOptional)
|
raw.Unset(lnwire.SimpleTaprootOverlayChansOptional)
|
||||||
raw.Unset(lnwire.SimpleTaprootOverlayChansRequired)
|
raw.Unset(lnwire.SimpleTaprootOverlayChansRequired)
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.NoExperimentalEndorsement {
|
if cfg.NoExperimentalEndorsement {
|
||||||
raw.Unset(lnwire.ExperimentalEndorsementOptional)
|
raw.Unset(lnwire.ExperimentalEndorsementOptional)
|
||||||
raw.Unset(lnwire.ExperimentalEndorsementRequired)
|
raw.Unset(lnwire.ExperimentalEndorsementRequired)
|
||||||
}
|
}
|
||||||
|
if cfg.NoRbfCoopClose {
|
||||||
|
raw.Unset(lnwire.RbfCoopCloseOptionalStaging)
|
||||||
|
}
|
||||||
|
|
||||||
for _, custom := range cfg.CustomFeatures[set] {
|
for _, custom := range cfg.CustomFeatures[set] {
|
||||||
if custom > set.Maximum() {
|
if custom > set.Maximum() {
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ type FlushHookID uint64
|
|||||||
|
|
||||||
// LinkDirection is used to query and change any link state on a per-direction
|
// LinkDirection is used to query and change any link state on a per-direction
|
||||||
// basis.
|
// basis.
|
||||||
type LinkDirection bool
|
type LinkDirection = bool
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Incoming is the direction from the remote peer to our node.
|
// Incoming is the direction from the remote peer to our node.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package htlcswitch
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
@@ -125,6 +126,9 @@ type ChanClose struct {
|
|||||||
|
|
||||||
// Err is used by request creator to receive request execution error.
|
// Err is used by request creator to receive request execution error.
|
||||||
Err chan error
|
Err chan error
|
||||||
|
|
||||||
|
// Ctx is a context linked to the lifetime of the caller.
|
||||||
|
Ctx context.Context //nolint:containedctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config defines the configuration for the service. ALL elements within the
|
// Config defines the configuration for the service. ALL elements within the
|
||||||
@@ -1413,7 +1417,7 @@ func (s *Switch) teardownCircuit(pkt *htlcPacket) error {
|
|||||||
// targetFeePerKw parameter should be the ideal fee-per-kw that will be used as
|
// targetFeePerKw parameter should be the ideal fee-per-kw that will be used as
|
||||||
// a starting point for close negotiation. The deliveryScript parameter is an
|
// a starting point for close negotiation. The deliveryScript parameter is an
|
||||||
// optional parameter which sets a user specified script to close out to.
|
// optional parameter which sets a user specified script to close out to.
|
||||||
func (s *Switch) CloseLink(chanPoint *wire.OutPoint,
|
func (s *Switch) CloseLink(ctx context.Context, chanPoint *wire.OutPoint,
|
||||||
closeType contractcourt.ChannelCloseType,
|
closeType contractcourt.ChannelCloseType,
|
||||||
targetFeePerKw, maxFee chainfee.SatPerKWeight,
|
targetFeePerKw, maxFee chainfee.SatPerKWeight,
|
||||||
deliveryScript lnwire.DeliveryAddress) (chan interface{}, chan error) {
|
deliveryScript lnwire.DeliveryAddress) (chan interface{}, chan error) {
|
||||||
@@ -1427,9 +1431,10 @@ func (s *Switch) CloseLink(chanPoint *wire.OutPoint,
|
|||||||
ChanPoint: chanPoint,
|
ChanPoint: chanPoint,
|
||||||
Updates: updateChan,
|
Updates: updateChan,
|
||||||
TargetFeePerKw: targetFeePerKw,
|
TargetFeePerKw: targetFeePerKw,
|
||||||
MaxFee: maxFee,
|
|
||||||
DeliveryScript: deliveryScript,
|
DeliveryScript: deliveryScript,
|
||||||
Err: errChan,
|
Err: errChan,
|
||||||
|
MaxFee: maxFee,
|
||||||
|
Ctx: ctx,
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package input
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/chaincfg"
|
||||||
|
"github.com/btcsuite/btcd/txscript"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FuzzScriptIsOpReturn fuzzes the ScriptIsOpReturn function.
|
||||||
|
func FuzzScriptIsOpReturn(f *testing.F) {
|
||||||
|
// Seed the corpus with some representative inputs.
|
||||||
|
f.Add([]byte{})
|
||||||
|
f.Add([]byte{txscript.OP_RETURN})
|
||||||
|
|
||||||
|
// Use our canonical ScriptBuilder to produce a valid OP_RETURN script.
|
||||||
|
builder := txscript.NewScriptBuilder()
|
||||||
|
builder.AddOp(txscript.OP_RETURN)
|
||||||
|
builder.AddData([]byte("valid data"))
|
||||||
|
script, err := builder.Script()
|
||||||
|
require.NoError(f, err)
|
||||||
|
f.Add(script)
|
||||||
|
|
||||||
|
// An example of a script that does not start with OP_RETURN.
|
||||||
|
f.Add([]byte{txscript.OP_DUP, txscript.OP_RETURN})
|
||||||
|
|
||||||
|
f.Fuzz(func(t *testing.T, pkScript []byte) {
|
||||||
|
result := ScriptIsOpReturn(pkScript)
|
||||||
|
|
||||||
|
var expected bool
|
||||||
|
if len(script) == 0 || script[0] != txscript.OP_RETURN {
|
||||||
|
expected = false
|
||||||
|
} else {
|
||||||
|
scriptClass, _, _, err := txscript.ExtractPkScriptAddrs(
|
||||||
|
pkScript, &chaincfg.MainNetParams,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to extract pk script "+
|
||||||
|
"addresses: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected = scriptClass == txscript.NullDataTy
|
||||||
|
}
|
||||||
|
|
||||||
|
if result != expected {
|
||||||
|
t.Fatalf("for script %x, expected ScriptIsOpReturn=%v;"+
|
||||||
|
" got %v", script, expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -3239,3 +3239,36 @@ func ComputeCommitmentPoint(commitSecret []byte) *btcec.PublicKey {
|
|||||||
_, pubKey := btcec.PrivKeyFromBytes(commitSecret)
|
_, pubKey := btcec.PrivKeyFromBytes(commitSecret)
|
||||||
return pubKey
|
return pubKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ScriptIsOpReturn returns true if the passed script is an OP_RETURN script.
|
||||||
|
//
|
||||||
|
// Lifted from the txscript package:
|
||||||
|
// https://github.com/btcsuite/btcd/blob/cc26860b40265e1332cca8748c5dbaf3c81cc094/txscript/standard.go#L493-L526.
|
||||||
|
//
|
||||||
|
//nolint:ll
|
||||||
|
func ScriptIsOpReturn(script []byte) bool {
|
||||||
|
// A null script is of the form:
|
||||||
|
// OP_RETURN <optional data>
|
||||||
|
//
|
||||||
|
// Thus, it can either be a single OP_RETURN or an OP_RETURN followed by
|
||||||
|
// a data push up to MaxDataCarrierSize bytes.
|
||||||
|
|
||||||
|
// The script can't possibly be a null data script if it doesn't start
|
||||||
|
// with OP_RETURN. Fail fast to avoid more work below.
|
||||||
|
if len(script) < 1 || script[0] != txscript.OP_RETURN {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single OP_RETURN.
|
||||||
|
if len(script) == 1 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// OP_RETURN followed by data push up to MaxDataCarrierSize bytes.
|
||||||
|
tokenizer := txscript.MakeScriptTokenizer(0, script[1:])
|
||||||
|
|
||||||
|
return tokenizer.Next() && tokenizer.Done() &&
|
||||||
|
(txscript.IsSmallInt(tokenizer.Opcode()) ||
|
||||||
|
tokenizer.Opcode() <= txscript.OP_PUSHDATA4) &&
|
||||||
|
len(tokenizer.Data()) <= txscript.MaxDataCarrierSize
|
||||||
|
}
|
||||||
@@ -678,6 +678,10 @@ var allTestCases = []*lntest.TestCase{
|
|||||||
Name: "access perm",
|
Name: "access perm",
|
||||||
TestFunc: testAccessPerm,
|
TestFunc: testAccessPerm,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "rbf coop close",
|
||||||
|
TestFunc: testCoopCloseRbf,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// appendPrefixed is used to add a prefix to each test name in the subtests
|
// appendPrefixed is used to add a prefix to each test name in the subtests
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
package itest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
|
"github.com/lightningnetwork/lnd/lntest"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testCoopCloseRbf(ht *lntest.HarnessTest) {
|
||||||
|
rbfCoopFlags := []string{"--protocol.rbf-coop-close"}
|
||||||
|
|
||||||
|
// Set the fee estimate to 1sat/vbyte. This ensures that our manually
|
||||||
|
// initiated RBF attempts will always be successful.
|
||||||
|
ht.SetFeeEstimate(250)
|
||||||
|
ht.SetFeeEstimateWithConf(250, 6)
|
||||||
|
|
||||||
|
// To kick things off, we'll create two new nodes, then fund them with
|
||||||
|
// enough coins to make a 50/50 channel.
|
||||||
|
cfgs := [][]string{rbfCoopFlags, rbfCoopFlags}
|
||||||
|
params := lntest.OpenChannelParams{
|
||||||
|
Amt: btcutil.Amount(1000000),
|
||||||
|
PushAmt: btcutil.Amount(1000000 / 2),
|
||||||
|
}
|
||||||
|
chanPoints, nodes := ht.CreateSimpleNetwork(cfgs, params)
|
||||||
|
alice, bob := nodes[0], nodes[1]
|
||||||
|
chanPoint := chanPoints[0]
|
||||||
|
|
||||||
|
// Now that both sides are active with a funded channel, we can kick
|
||||||
|
// off the test.
|
||||||
|
//
|
||||||
|
// To start, we'll have Alice try to close the channel, with a fee rate
|
||||||
|
// of 5 sat/byte.
|
||||||
|
aliceFeeRate := chainfee.SatPerVByte(5)
|
||||||
|
aliceCloseStream, aliceCloseUpdate := ht.CloseChannelAssertPending(
|
||||||
|
alice, chanPoint, false,
|
||||||
|
lntest.WithCoopCloseFeeRate(aliceFeeRate),
|
||||||
|
lntest.WithLocalTxNotify(),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Confirm that this new update was at 5 sat/vb.
|
||||||
|
alicePendingUpdate := aliceCloseUpdate.GetClosePending()
|
||||||
|
require.NotNil(ht, aliceCloseUpdate)
|
||||||
|
require.Equal(
|
||||||
|
ht, int64(aliceFeeRate), alicePendingUpdate.FeePerVbyte,
|
||||||
|
)
|
||||||
|
require.True(ht, alicePendingUpdate.LocalCloseTx)
|
||||||
|
|
||||||
|
// Now, we'll have Bob attempt to RBF the close transaction with a
|
||||||
|
// higher fee rate, double that of Alice's.
|
||||||
|
bobFeeRate := aliceFeeRate * 2
|
||||||
|
bobCloseStream, bobCloseUpdate := ht.CloseChannelAssertPending(
|
||||||
|
bob, chanPoint, false, lntest.WithCoopCloseFeeRate(bobFeeRate),
|
||||||
|
lntest.WithLocalTxNotify(),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Confirm that this new update was at 10 sat/vb.
|
||||||
|
bobPendingUpdate := bobCloseUpdate.GetClosePending()
|
||||||
|
require.NotNil(ht, bobCloseUpdate)
|
||||||
|
require.Equal(ht, bobPendingUpdate.FeePerVbyte, int64(bobFeeRate))
|
||||||
|
require.True(ht, bobPendingUpdate.LocalCloseTx)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Alice should've also received a similar update that Bob has
|
||||||
|
// increased the closing fee rate to 10 sat/vb with his settled funds.
|
||||||
|
aliceCloseUpdate, err = ht.ReceiveCloseChannelUpdate(aliceCloseStream)
|
||||||
|
require.NoError(ht, err)
|
||||||
|
alicePendingUpdate = aliceCloseUpdate.GetClosePending()
|
||||||
|
require.NotNil(ht, aliceCloseUpdate)
|
||||||
|
require.Equal(ht, alicePendingUpdate.FeePerVbyte, int64(bobFeeRate))
|
||||||
|
require.False(ht, alicePendingUpdate.LocalCloseTx)
|
||||||
|
|
||||||
|
// We'll now attempt to make a fee update that increases Alice's fee
|
||||||
|
// rate by 6 sat/vb, which should be rejected as it is too small of an
|
||||||
|
// increase for the RBF rules. The RPC API however will return the new
|
||||||
|
// fee. We'll skip the mempool check here as it won't make it in.
|
||||||
|
aliceRejectedFeeRate := aliceFeeRate + 1
|
||||||
|
_, aliceCloseUpdate = ht.CloseChannelAssertPending(
|
||||||
|
alice, chanPoint, false,
|
||||||
|
lntest.WithCoopCloseFeeRate(aliceRejectedFeeRate),
|
||||||
|
lntest.WithLocalTxNotify(), lntest.WithSkipMempoolCheck(),
|
||||||
|
)
|
||||||
|
alicePendingUpdate = aliceCloseUpdate.GetClosePending()
|
||||||
|
require.NotNil(ht, aliceCloseUpdate)
|
||||||
|
require.Equal(
|
||||||
|
ht, alicePendingUpdate.FeePerVbyte,
|
||||||
|
int64(aliceRejectedFeeRate),
|
||||||
|
)
|
||||||
|
require.True(ht, alicePendingUpdate.LocalCloseTx)
|
||||||
|
|
||||||
|
_, err = ht.ReceiveCloseChannelUpdate(bobCloseStream)
|
||||||
|
require.NoError(ht, err)
|
||||||
|
|
||||||
|
// We'll now attempt a fee update that we can't actually pay for. This
|
||||||
|
// will actually show up as an error to the remote party.
|
||||||
|
aliceRejectedFeeRate = 100_000
|
||||||
|
_, _ = ht.CloseChannelAssertPending(
|
||||||
|
alice, chanPoint, false,
|
||||||
|
lntest.WithCoopCloseFeeRate(aliceRejectedFeeRate),
|
||||||
|
lntest.WithLocalTxNotify(),
|
||||||
|
lntest.WithExpectedErrString("cannot pay for fee"),
|
||||||
|
)
|
||||||
|
|
||||||
|
// At this point, we'll have Alice+Bob reconnect so we can ensure that
|
||||||
|
// we can continue to do RBF bumps even after a reconnection.
|
||||||
|
ht.DisconnectNodes(alice, bob)
|
||||||
|
ht.ConnectNodes(alice, bob)
|
||||||
|
|
||||||
|
// Next, we'll have Alice double that fee rate again to 20 sat/vb.
|
||||||
|
aliceFeeRate = bobFeeRate * 2
|
||||||
|
aliceCloseStream, aliceCloseUpdate = ht.CloseChannelAssertPending(
|
||||||
|
alice, chanPoint, false,
|
||||||
|
lntest.WithCoopCloseFeeRate(aliceFeeRate),
|
||||||
|
lntest.WithLocalTxNotify(),
|
||||||
|
)
|
||||||
|
|
||||||
|
alicePendingUpdate = aliceCloseUpdate.GetClosePending()
|
||||||
|
require.NotNil(ht, aliceCloseUpdate)
|
||||||
|
require.Equal(
|
||||||
|
ht, alicePendingUpdate.FeePerVbyte, int64(aliceFeeRate),
|
||||||
|
)
|
||||||
|
require.True(ht, alicePendingUpdate.LocalCloseTx)
|
||||||
|
|
||||||
|
// To conclude, we'll mine a block which should now confirm Alice's
|
||||||
|
// version of the coop close transaction.
|
||||||
|
block := ht.MineBlocksAndAssertNumTxes(1, 1)[0]
|
||||||
|
|
||||||
|
// Both Alice and Bob should trigger a final close update to signal the
|
||||||
|
// closing transaction has confirmed.
|
||||||
|
aliceClosingTxid := ht.WaitForChannelCloseEvent(aliceCloseStream)
|
||||||
|
ht.AssertTxInBlock(block, aliceClosingTxid)
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package itest
|
package itest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/btcutil"
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
@@ -10,6 +11,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||||
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
||||||
"github.com/lightningnetwork/lnd/lntest"
|
"github.com/lightningnetwork/lnd/lntest"
|
||||||
|
"github.com/lightningnetwork/lnd/lntest/node"
|
||||||
"github.com/lightningnetwork/lnd/lntest/wait"
|
"github.com/lightningnetwork/lnd/lntest/wait"
|
||||||
"github.com/lightningnetwork/lnd/lntypes"
|
"github.com/lightningnetwork/lnd/lntypes"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||||
@@ -23,23 +25,47 @@ import (
|
|||||||
// will have the receiver settle the invoice and observe that the channel gets
|
// will have the receiver settle the invoice and observe that the channel gets
|
||||||
// torn down after settlement.
|
// torn down after settlement.
|
||||||
func testCoopCloseWithHtlcs(ht *lntest.HarnessTest) {
|
func testCoopCloseWithHtlcs(ht *lntest.HarnessTest) {
|
||||||
ht.Run("no restart", func(t *testing.T) {
|
rbfCoopFlags := []string{"--protocol.rbf-coop-close"}
|
||||||
tt := ht.Subtest(t)
|
|
||||||
coopCloseWithHTLCs(tt)
|
|
||||||
})
|
|
||||||
|
|
||||||
ht.Run("with restart", func(t *testing.T) {
|
for _, isRbf := range []bool{true, false} {
|
||||||
|
testName := fmt.Sprintf("no restart is_rbf=%v", isRbf)
|
||||||
|
ht.Run(testName, func(t *testing.T) {
|
||||||
tt := ht.Subtest(t)
|
tt := ht.Subtest(t)
|
||||||
coopCloseWithHTLCsWithRestart(tt)
|
|
||||||
|
var flags []string
|
||||||
|
if isRbf {
|
||||||
|
flags = rbfCoopFlags
|
||||||
|
}
|
||||||
|
|
||||||
|
alice := ht.NewNodeWithCoins("Alice", flags)
|
||||||
|
bob := ht.NewNodeWithCoins("bob", flags)
|
||||||
|
|
||||||
|
coopCloseWithHTLCs(tt, alice, bob)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, isRbf := range []bool{true, false} {
|
||||||
|
testName := fmt.Sprintf("with restart is_rbf=%v", isRbf)
|
||||||
|
ht.Run(testName, func(t *testing.T) {
|
||||||
|
tt := ht.Subtest(t)
|
||||||
|
|
||||||
|
var flags []string
|
||||||
|
if isRbf {
|
||||||
|
flags = rbfCoopFlags
|
||||||
|
}
|
||||||
|
|
||||||
|
alice := ht.NewNodeWithCoins("Alice", flags)
|
||||||
|
bob := ht.NewNodeWithCoins("bob", flags)
|
||||||
|
|
||||||
|
coopCloseWithHTLCsWithRestart(tt, alice, bob)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// coopCloseWithHTLCs tests the basic coop close scenario which occurs when one
|
// coopCloseWithHTLCs tests the basic coop close scenario which occurs when one
|
||||||
// channel party initiates a channel shutdown while an HTLC is still pending on
|
// channel party initiates a channel shutdown while an HTLC is still pending on
|
||||||
// the channel.
|
// the channel.
|
||||||
func coopCloseWithHTLCs(ht *lntest.HarnessTest) {
|
func coopCloseWithHTLCs(ht *lntest.HarnessTest, alice, bob *node.HarnessNode) {
|
||||||
alice := ht.NewNodeWithCoins("Alice", nil)
|
|
||||||
bob := ht.NewNodeWithCoins("bob", nil)
|
|
||||||
ht.ConnectNodes(alice, bob)
|
ht.ConnectNodes(alice, bob)
|
||||||
|
|
||||||
// Here we set up a channel between Alice and Bob, beginning with a
|
// Here we set up a channel between Alice and Bob, beginning with a
|
||||||
@@ -131,9 +157,9 @@ func coopCloseWithHTLCs(ht *lntest.HarnessTest) {
|
|||||||
// is still pending on the channel but this time it ensures that the shutdown
|
// is still pending on the channel but this time it ensures that the shutdown
|
||||||
// process continues as expected even if a channel re-establish happens after
|
// process continues as expected even if a channel re-establish happens after
|
||||||
// one party has already initiated the shutdown.
|
// one party has already initiated the shutdown.
|
||||||
func coopCloseWithHTLCsWithRestart(ht *lntest.HarnessTest) {
|
func coopCloseWithHTLCsWithRestart(ht *lntest.HarnessTest, alice,
|
||||||
alice := ht.NewNodeWithCoins("Alice", nil)
|
bob *node.HarnessNode) {
|
||||||
bob := ht.NewNodeWithCoins("bob", nil)
|
|
||||||
ht.ConnectNodes(alice, bob)
|
ht.ConnectNodes(alice, bob)
|
||||||
|
|
||||||
// Open a channel between Alice and Bob with the balance split equally.
|
// Open a channel between Alice and Bob with the balance split equally.
|
||||||
@@ -219,8 +245,8 @@ func coopCloseWithHTLCsWithRestart(ht *lntest.HarnessTest) {
|
|||||||
}, defaultTimeout)
|
}, defaultTimeout)
|
||||||
require.NoError(ht, err)
|
require.NoError(ht, err)
|
||||||
|
|
||||||
// Wait for the close tx to be in the Mempool and then mine 6 blocks
|
// Wait for the close tx to be in the Mempool and then mine 6 blocks to
|
||||||
// to confirm the close.
|
// confirm the close.
|
||||||
closingTx := ht.AssertClosingTxInMempool(
|
closingTx := ht.AssertClosingTxInMempool(
|
||||||
chanPoint, lnrpc.CommitmentType_LEGACY,
|
chanPoint, lnrpc.CommitmentType_LEGACY,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -109,7 +109,11 @@ func breachRetributionTestCase(ht *lntest.HarnessTest,
|
|||||||
// broadcasting his current channel state. This is actually the
|
// broadcasting his current channel state. This is actually the
|
||||||
// commitment transaction of a prior *revoked* state, so he'll soon
|
// commitment transaction of a prior *revoked* state, so he'll soon
|
||||||
// feel the wrath of Carol's retribution.
|
// feel the wrath of Carol's retribution.
|
||||||
_, breachTXID := ht.CloseChannelAssertPending(bob, chanPoint, true)
|
_, breachCloseUpd := ht.CloseChannelAssertPending(bob, chanPoint, true)
|
||||||
|
closeUpd := breachCloseUpd.GetClosePending()
|
||||||
|
require.NotNil(ht, closeUpd)
|
||||||
|
breachTXID, err := chainhash.NewHash(closeUpd.Txid)
|
||||||
|
require.NoError(ht, err)
|
||||||
|
|
||||||
// Here, Carol sees Bob's breach transaction in the mempool, but is
|
// Here, Carol sees Bob's breach transaction in the mempool, but is
|
||||||
// waiting for it to confirm before continuing her retribution. We
|
// waiting for it to confirm before continuing her retribution. We
|
||||||
@@ -122,13 +126,13 @@ func breachRetributionTestCase(ht *lntest.HarnessTest,
|
|||||||
// update, then ensure that the closing transaction was included in the
|
// update, then ensure that the closing transaction was included in the
|
||||||
// block.
|
// block.
|
||||||
block := ht.MineBlocksAndAssertNumTxes(1, 1)[0]
|
block := ht.MineBlocksAndAssertNumTxes(1, 1)[0]
|
||||||
ht.AssertTxInBlock(block, breachTXID)
|
ht.AssertTxInBlock(block, *breachTXID)
|
||||||
|
|
||||||
// Construct to_remote output which pays to Bob. Based on the output
|
// Construct to_remote output which pays to Bob. Based on the output
|
||||||
// ordering, the first output in this breach tx is the to_remote
|
// ordering, the first output in this breach tx is the to_remote
|
||||||
// output.
|
// output.
|
||||||
toRemoteOp := wire.OutPoint{
|
toRemoteOp := wire.OutPoint{
|
||||||
Hash: breachTXID,
|
Hash: *breachTXID,
|
||||||
Index: 0,
|
Index: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,7 +155,7 @@ func breachRetributionTestCase(ht *lntest.HarnessTest,
|
|||||||
// Assert that all the inputs of this transaction are spending outputs
|
// Assert that all the inputs of this transaction are spending outputs
|
||||||
// generated by Bob's breach transaction above.
|
// generated by Bob's breach transaction above.
|
||||||
for _, txIn := range justiceTx.TxIn {
|
for _, txIn := range justiceTx.TxIn {
|
||||||
require.Equal(ht, breachTXID, txIn.PreviousOutPoint.Hash,
|
require.Equal(ht, *breachTXID, txIn.PreviousOutPoint.Hash,
|
||||||
"justice tx not spending commitment utxo")
|
"justice tx not spending commitment utxo")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,7 +300,7 @@ func revokedCloseRetributionZeroValueRemoteOutputCase(ht *lntest.HarnessTest,
|
|||||||
// broadcasting her current channel state. This is actually the
|
// broadcasting her current channel state. This is actually the
|
||||||
// commitment transaction of a prior *revoked* state, so she'll soon
|
// commitment transaction of a prior *revoked* state, so she'll soon
|
||||||
// feel the wrath of Dave's retribution.
|
// feel the wrath of Dave's retribution.
|
||||||
stream, closeTxID := ht.CloseChannelAssertPending(
|
stream, closeUpdate := ht.CloseChannelAssertPending(
|
||||||
carol, chanPoint, true,
|
carol, chanPoint, true,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -310,9 +314,11 @@ func revokedCloseRetributionZeroValueRemoteOutputCase(ht *lntest.HarnessTest,
|
|||||||
// state and continues exacting justice after his node restarts.
|
// state and continues exacting justice after his node restarts.
|
||||||
ht.RestartNode(dave)
|
ht.RestartNode(dave)
|
||||||
|
|
||||||
|
closeTxID := closeUpdate.GetClosePending().Txid
|
||||||
|
|
||||||
// The breachTXID should match the above closeTxID.
|
// The breachTXID should match the above closeTxID.
|
||||||
breachTXID := ht.WaitForChannelCloseEvent(stream)
|
breachTXID := ht.WaitForChannelCloseEvent(stream)
|
||||||
require.EqualValues(ht, breachTXID, closeTxID)
|
require.EqualValues(ht, breachTXID[:], closeTxID)
|
||||||
|
|
||||||
// Construct to_local output which pays to Dave. Based on the output
|
// Construct to_local output which pays to Dave. Based on the output
|
||||||
// ordering, the first output in this breach tx is the to_local
|
// ordering, the first output in this breach tx is the to_local
|
||||||
@@ -543,9 +549,13 @@ func revokedCloseRetributionRemoteHodlCase(ht *lntest.HarnessTest,
|
|||||||
// broadcasting her current channel state. This is actually the
|
// broadcasting her current channel state. This is actually the
|
||||||
// commitment transaction of a prior *revoked* state, so she'll soon
|
// commitment transaction of a prior *revoked* state, so she'll soon
|
||||||
// feel the wrath of Dave's retribution.
|
// feel the wrath of Dave's retribution.
|
||||||
closeUpdates, closeTxID := ht.CloseChannelAssertPending(
|
closeUpdates, closeUpd := ht.CloseChannelAssertPending(
|
||||||
carol, chanPoint, true,
|
carol, chanPoint, true,
|
||||||
)
|
)
|
||||||
|
pendingCloseUpd := closeUpd.GetClosePending()
|
||||||
|
require.NotNil(ht, pendingCloseUpd)
|
||||||
|
closeTxID, err := chainhash.NewHash(pendingCloseUpd.Txid)
|
||||||
|
require.NoError(ht, err)
|
||||||
|
|
||||||
// Generate a single block to mine the breach transaction.
|
// Generate a single block to mine the breach transaction.
|
||||||
block := ht.MineBlocksAndAssertNumTxes(1, 1)[0]
|
block := ht.MineBlocksAndAssertNumTxes(1, 1)[0]
|
||||||
@@ -593,7 +603,7 @@ func revokedCloseRetributionRemoteHodlCase(ht *lntest.HarnessTest,
|
|||||||
return nil, errNotFound
|
return nil, errNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
err := wait.NoError(func() error {
|
err = wait.NoError(func() error {
|
||||||
txid, err := findJusticeTx()
|
txid, err := findJusticeTx()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/btcutil"
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
"github.com/lightningnetwork/lnd/contractcourt"
|
"github.com/lightningnetwork/lnd/contractcourt"
|
||||||
"github.com/lightningnetwork/lnd/fn/v2"
|
"github.com/lightningnetwork/lnd/fn/v2"
|
||||||
@@ -1963,8 +1964,8 @@ func testBumpForceCloseFee(ht *lntest.HarnessTest) {
|
|||||||
ht.FundCoinsP2TR(btcutil.SatoshiPerBitcoin, alice)
|
ht.FundCoinsP2TR(btcutil.SatoshiPerBitcoin, alice)
|
||||||
|
|
||||||
// Alice force closes the channel which has no HTLCs at stake.
|
// Alice force closes the channel which has no HTLCs at stake.
|
||||||
_, closingTxID := ht.CloseChannelAssertPending(alice, chanPoint, true)
|
_, closeUpdates := ht.CloseChannelAssertPending(alice, chanPoint, true)
|
||||||
require.NotNil(ht, closingTxID)
|
require.NotNil(ht, closeUpdates)
|
||||||
|
|
||||||
// Alice should see one waiting close channel.
|
// Alice should see one waiting close channel.
|
||||||
ht.AssertNumWaitingClose(alice, 1)
|
ht.AssertNumWaitingClose(alice, 1)
|
||||||
@@ -1974,7 +1975,10 @@ func testBumpForceCloseFee(ht *lntest.HarnessTest) {
|
|||||||
ht.AssertNumPendingSweeps(alice, 2)
|
ht.AssertNumPendingSweeps(alice, 2)
|
||||||
|
|
||||||
// Calculate the commitment tx fee rate.
|
// Calculate the commitment tx fee rate.
|
||||||
closingTx := ht.AssertTxInMempool(closingTxID)
|
pendingClose := closeUpdates.GetClosePending()
|
||||||
|
closeTxid, err := chainhash.NewHash(pendingClose.Txid)
|
||||||
|
require.NoError(ht, err)
|
||||||
|
closingTx := ht.AssertTxInMempool(*closeTxid)
|
||||||
require.NotNil(ht, closingTx)
|
require.NotNil(ht, closingTx)
|
||||||
|
|
||||||
// The default commitment fee for anchor channels is capped at 2500
|
// The default commitment fee for anchor channels is capped at 2500
|
||||||
|
|||||||
@@ -491,7 +491,7 @@ func testRevokedCloseRetributionAltruistWatchtowerCase(ht *lntest.HarnessTest,
|
|||||||
// broadcasting his current channel state. This is actually the
|
// broadcasting his current channel state. This is actually the
|
||||||
// commitment transaction of a prior *revoked* state, so he'll soon
|
// commitment transaction of a prior *revoked* state, so he'll soon
|
||||||
// feel the wrath of Dave's retribution.
|
// feel the wrath of Dave's retribution.
|
||||||
closeUpdates, closeTxID := ht.CloseChannelAssertPending(
|
closeUpdates, pendingClose := ht.CloseChannelAssertPending(
|
||||||
carol, chanPoint, true,
|
carol, chanPoint, true,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -504,7 +504,8 @@ func testRevokedCloseRetributionAltruistWatchtowerCase(ht *lntest.HarnessTest,
|
|||||||
ht.AssertTxInBlock(block, breachTXID)
|
ht.AssertTxInBlock(block, breachTXID)
|
||||||
|
|
||||||
// The breachTXID should match the above closeTxID.
|
// The breachTXID should match the above closeTxID.
|
||||||
require.EqualValues(ht, breachTXID, closeTxID)
|
closeTxID := pendingClose.GetClosePending().Txid
|
||||||
|
require.EqualValues(ht, breachTXID[:], closeTxID)
|
||||||
|
|
||||||
// Query the mempool for Dave's justice transaction, this should be
|
// Query the mempool for Dave's justice transaction, this should be
|
||||||
// broadcast as Carol's contract breaching transaction gets confirmed
|
// broadcast as Carol's contract breaching transaction gets confirmed
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ type ProtocolOptions struct {
|
|||||||
// the experimental taproot overlay chan type.
|
// the experimental taproot overlay chan type.
|
||||||
TaprootOverlayChans bool `long:"simple-taproot-overlay-chans" description:"if set, then lnd will create and accept requests for channels using the taproot overlay commitment type"`
|
TaprootOverlayChans bool `long:"simple-taproot-overlay-chans" description:"if set, then lnd will create and accept requests for channels using the taproot overlay commitment type"`
|
||||||
|
|
||||||
|
// RbfCoopClose should be set if we want to signal that we support for
|
||||||
|
// the new experimental RBF coop close feature.
|
||||||
|
RbfCoopClose bool `long:"rbf-coop-close" description:"if set, then lnd will signal that it supports the new RBF based coop close protocol"`
|
||||||
|
|
||||||
// NoAnchors should be set if we don't want to support opening or accepting
|
// NoAnchors should be set if we don't want to support opening or accepting
|
||||||
// channels having the anchor commitment type.
|
// channels having the anchor commitment type.
|
||||||
NoAnchors bool `long:"no-anchors" description:"disable support for anchor commitments"`
|
NoAnchors bool `long:"no-anchors" description:"disable support for anchor commitments"`
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ type ProtocolOptions struct {
|
|||||||
// TODO(halseth): transition itests to anchors instead!
|
// TODO(halseth): transition itests to anchors instead!
|
||||||
Anchors bool `long:"anchors" description:"enable support for anchor commitments"`
|
Anchors bool `long:"anchors" description:"enable support for anchor commitments"`
|
||||||
|
|
||||||
|
// RbfCoopClose should be set if we want to signal that we support for
|
||||||
|
// the new experimental RBF coop close feature.
|
||||||
|
RbfCoopClose bool `long:"rbf-coop-close" description:"if set, then lnd will signal that it supports the new RBF based coop close protocol"`
|
||||||
|
|
||||||
// ScriptEnforcedLease enables script enforced commitments for channel
|
// ScriptEnforcedLease enables script enforced commitments for channel
|
||||||
// leases.
|
// leases.
|
||||||
//
|
//
|
||||||
|
|||||||
+2284
-2263
File diff suppressed because it is too large.
Load diff
@@ -2167,6 +2167,8 @@ message CloseStatusUpdate {
|
|||||||
message PendingUpdate {
|
message PendingUpdate {
|
||||||
bytes txid = 1;
|
bytes txid = 1;
|
||||||
uint32 output_index = 2;
|
uint32 output_index = 2;
|
||||||
|
int64 fee_per_vbyte = 3;
|
||||||
|
bool local_close_tx = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message InstantUpdate {
|
message InstantUpdate {
|
||||||
|
|||||||
@@ -6739,6 +6739,13 @@
|
|||||||
"output_index": {
|
"output_index": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "int64"
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"fee_per_vbyte": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"local_close_tx": {
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+102
-11
@@ -1208,14 +1208,79 @@ func (h *HarnessTest) OpenChannelAssertErr(srcNode, destNode *node.HarnessNode,
|
|||||||
"error returned, want %v, got %v", expectedErr, err)
|
"error returned, want %v, got %v", expectedErr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// closeChannelOpts holds the options for closing a channel.
|
||||||
|
type closeChannelOpts struct {
|
||||||
|
feeRate fn.Option[chainfee.SatPerVByte]
|
||||||
|
|
||||||
|
// localTxOnly is a boolean indicating if we should only attempt to
|
||||||
|
// consume close pending notifications for the local transaction.
|
||||||
|
localTxOnly bool
|
||||||
|
|
||||||
|
// skipMempoolCheck is a boolean indicating if we should skip the normal
|
||||||
|
// mempool check after a coop close.
|
||||||
|
skipMempoolCheck bool
|
||||||
|
|
||||||
|
// errString is an expected error. If this is non-blank, then we'll
|
||||||
|
// assert that the coop close wasn't possible, and returns an error that
|
||||||
|
// contains this err string.
|
||||||
|
errString string
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseChanOpt is a functional option to modify the way we close a channel.
|
||||||
|
type CloseChanOpt func(*closeChannelOpts)
|
||||||
|
|
||||||
|
// WithCoopCloseFeeRate is a functional option to set the fee rate for a coop
|
||||||
|
// close attempt.
|
||||||
|
func WithCoopCloseFeeRate(rate chainfee.SatPerVByte) CloseChanOpt {
|
||||||
|
return func(o *closeChannelOpts) {
|
||||||
|
o.feeRate = fn.Some(rate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLocalTxNotify is a functional option to indicate that we should only
|
||||||
|
// notify for the local txn. This is useful for the RBF coop close type, as
|
||||||
|
// it'll notify for both local and remote txns.
|
||||||
|
func WithLocalTxNotify() CloseChanOpt {
|
||||||
|
return func(o *closeChannelOpts) {
|
||||||
|
o.localTxOnly = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithSkipMempoolCheck is a functional option to indicate that we should skip
|
||||||
|
// the mempool check. This can be used when a coop close iteration may not
|
||||||
|
// result in a newly broadcast transaction.
|
||||||
|
func WithSkipMempoolCheck() CloseChanOpt {
|
||||||
|
return func(o *closeChannelOpts) {
|
||||||
|
o.skipMempoolCheck = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithExpectedErrString is a functional option that can be used to assert that
|
||||||
|
// an error occurs during the coop close process.
|
||||||
|
func WithExpectedErrString(errString string) CloseChanOpt {
|
||||||
|
return func(o *closeChannelOpts) {
|
||||||
|
o.errString = errString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaultCloseOpts returns the set of default close options.
|
||||||
|
func defaultCloseOpts() *closeChannelOpts {
|
||||||
|
return &closeChannelOpts{}
|
||||||
|
}
|
||||||
|
|
||||||
// CloseChannelAssertPending attempts to close the channel indicated by the
|
// CloseChannelAssertPending attempts to close the channel indicated by the
|
||||||
// passed channel point, initiated by the passed node. Once the CloseChannel
|
// passed channel point, initiated by the passed node. Once the CloseChannel
|
||||||
// rpc is called, it will consume one event and assert it's a close pending
|
// rpc is called, it will consume one event and assert it's a close pending
|
||||||
// event. In addition, it will check that the closing tx can be found in the
|
// event. In addition, it will check that the closing tx can be found in the
|
||||||
// mempool.
|
// mempool.
|
||||||
func (h *HarnessTest) CloseChannelAssertPending(hn *node.HarnessNode,
|
func (h *HarnessTest) CloseChannelAssertPending(hn *node.HarnessNode,
|
||||||
cp *lnrpc.ChannelPoint,
|
cp *lnrpc.ChannelPoint, force bool,
|
||||||
force bool) (rpc.CloseChanClient, chainhash.Hash) {
|
opts ...CloseChanOpt) (rpc.CloseChanClient, *lnrpc.CloseStatusUpdate) {
|
||||||
|
|
||||||
|
closeOpts := defaultCloseOpts()
|
||||||
|
for _, optFunc := range opts {
|
||||||
|
optFunc(closeOpts)
|
||||||
|
}
|
||||||
|
|
||||||
// Calls the rpc to close the channel.
|
// Calls the rpc to close the channel.
|
||||||
closeReq := &lnrpc.CloseChannelRequest{
|
closeReq := &lnrpc.CloseChannelRequest{
|
||||||
@@ -1224,10 +1289,9 @@ func (h *HarnessTest) CloseChannelAssertPending(hn *node.HarnessNode,
|
|||||||
NoWait: true,
|
NoWait: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// For coop close, we use a default confg target of 6.
|
closeOpts.feeRate.WhenSome(func(feeRate chainfee.SatPerVByte) {
|
||||||
if !force {
|
closeReq.SatPerVbyte = uint64(feeRate)
|
||||||
closeReq.TargetConf = 6
|
})
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
stream rpc.CloseChanClient
|
stream rpc.CloseChanClient
|
||||||
@@ -1242,25 +1306,52 @@ func (h *HarnessTest) CloseChannelAssertPending(hn *node.HarnessNode,
|
|||||||
_, err = h.ReceiveCloseChannelUpdate(stream)
|
_, err = h.ReceiveCloseChannelUpdate(stream)
|
||||||
require.NoError(h, err, "close channel update got error: %v", err)
|
require.NoError(h, err, "close channel update got error: %v", err)
|
||||||
|
|
||||||
|
var closeTxid *chainhash.Hash
|
||||||
|
for {
|
||||||
event, err = h.ReceiveCloseChannelUpdate(stream)
|
event, err = h.ReceiveCloseChannelUpdate(stream)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.Logf("Test: %s, close channel got error: %v",
|
h.Logf("Test: %s, close channel got error: %v",
|
||||||
h.manager.currentTestCase, err)
|
h.manager.currentTestCase, err)
|
||||||
}
|
}
|
||||||
|
if err != nil && closeOpts.errString == "" {
|
||||||
require.NoError(h, err, "retry closing channel failed")
|
require.NoError(h, err, "retry closing channel failed")
|
||||||
|
} else if err != nil && closeOpts.errString != "" {
|
||||||
|
require.ErrorContains(h, err, closeOpts.errString)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
pendingClose, ok := event.Update.(*lnrpc.CloseStatusUpdate_ClosePending)
|
pendingClose, ok := event.Update.(*lnrpc.CloseStatusUpdate_ClosePending) //nolint:ll
|
||||||
require.Truef(h, ok, "expected channel close update, instead got %v",
|
require.Truef(h, ok, "expected channel close "+
|
||||||
pendingClose)
|
"update, instead got %v", pendingClose)
|
||||||
|
|
||||||
closeTxid, err := chainhash.NewHash(pendingClose.ClosePending.Txid)
|
if !pendingClose.ClosePending.LocalCloseTx &&
|
||||||
|
closeOpts.localTxOnly {
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyRate := pendingClose.ClosePending.FeePerVbyte
|
||||||
|
if closeOpts.localTxOnly &&
|
||||||
|
notifyRate != int64(closeReq.SatPerVbyte) {
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
closeTxid, err = chainhash.NewHash(
|
||||||
|
pendingClose.ClosePending.Txid,
|
||||||
|
)
|
||||||
require.NoErrorf(h, err, "unable to decode closeTxid: %v",
|
require.NoErrorf(h, err, "unable to decode closeTxid: %v",
|
||||||
pendingClose.ClosePending.Txid)
|
pendingClose.ClosePending.Txid)
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if !closeOpts.skipMempoolCheck {
|
||||||
// Assert the closing tx is in the mempool.
|
// Assert the closing tx is in the mempool.
|
||||||
h.miner.AssertTxInMempool(*closeTxid)
|
h.miner.AssertTxInMempool(*closeTxid)
|
||||||
|
}
|
||||||
|
|
||||||
return stream, *closeTxid
|
return stream, event
|
||||||
}
|
}
|
||||||
|
|
||||||
// CloseChannel attempts to coop close a non-anchored channel identified by the
|
// CloseChannel attempts to coop close a non-anchored channel identified by the
|
||||||
|
|||||||
@@ -534,8 +534,8 @@ func (h HarnessTest) WaitForChannelCloseEvent(
|
|||||||
require.NoError(h, err)
|
require.NoError(h, err)
|
||||||
|
|
||||||
resp, ok := event.Update.(*lnrpc.CloseStatusUpdate_ChanClose)
|
resp, ok := event.Update.(*lnrpc.CloseStatusUpdate_ChanClose)
|
||||||
require.Truef(h, ok, "expected channel open update, instead got %v",
|
require.Truef(h, ok, "expected channel close update, instead got %v",
|
||||||
resp)
|
event.Update)
|
||||||
|
|
||||||
txid, err := chainhash.NewHash(resp.ChanClose.ClosingTxid)
|
txid, err := chainhash.NewHash(resp.ChanClose.ClosingTxid)
|
||||||
require.NoErrorf(h, err, "wrong format found in closing txid: %v",
|
require.NoErrorf(h, err, "wrong format found in closing txid: %v",
|
||||||
|
|||||||
@@ -0,0 +1,219 @@
|
|||||||
|
# RBF Co-op Close State Machine
|
||||||
|
|
||||||
|
## Abstract
|
||||||
|
|
||||||
|
The Lightning Network Daemon (lnd) implements a cooperative channel closing
|
||||||
|
mechanism that allows two connected peers to negotiate a mutually agreed-upon
|
||||||
|
closing transaction. This document outlines the state machine that governs the
|
||||||
|
cooperative closing process, defining the various states a channel progresses
|
||||||
|
through from active operation to final closure.
|
||||||
|
|
||||||
|
Cooperative closing is distinct from force closing in that it requires both
|
||||||
|
channel participants to agree on the final channel state and closing
|
||||||
|
transaction, resulting in lower fees and immediate fund availability compared to
|
||||||
|
unilateral channel closures.
|
||||||
|
|
||||||
|
## State Machine Overview
|
||||||
|
|
||||||
|
The cooperative closing state machine manages the lifecycle of a channel from
|
||||||
|
the moment a closing request is initiated until the closing transaction is
|
||||||
|
confirmed on the blockchain. The state transitions ensure proper negotiation of
|
||||||
|
closing fees, validation of signatures, and handling of protocol violations.
|
||||||
|
|
||||||
|
### High-Level Flow
|
||||||
|
|
||||||
|
1. An active channel receives a shutdown request (locally or remotely)
|
||||||
|
2. Channel stops accepting new HTLCs and begins flushing existing ones
|
||||||
|
3. Once flushed, nodes begin negotiating closing transaction fee
|
||||||
|
4. Upon agreement, nodes sign and broadcast the transaction
|
||||||
|
5. **RBF Iteration (optional)**: While waiting for confirmation, either node can
|
||||||
|
propose a fee increase via Replace-By-Fee (RBF), restarting the negotiation
|
||||||
|
process with higher fees
|
||||||
|
6. Process completes when transaction confirms on-chain
|
||||||
|
|
||||||
|
## States and Transitions
|
||||||
|
```mermaid
|
||||||
|
---
|
||||||
|
title: Co-Op Close V2
|
||||||
|
---
|
||||||
|
stateDiagram-v2
|
||||||
|
state CoopCloseV2 {
|
||||||
|
[*] --> ChannelActive
|
||||||
|
ChannelActive --> ShutdownPending: send_shutdown
|
||||||
|
ChannelActive --> ShutdownPending: shutdown_received
|
||||||
|
ShutdownPending --> ChannelFlushing: shutdown_received
|
||||||
|
ShutdownPending --> ChannelFlushing: shutdown_complete
|
||||||
|
|
||||||
|
ChannelFlushing --> ClosingNegotiation: channel_flushed
|
||||||
|
|
||||||
|
state ClosingNegotiation {
|
||||||
|
|
||||||
|
state LocalSide {
|
||||||
|
[*] --> LocalCloseStart
|
||||||
|
LocalCloseStart --> LocalOfferSent: send_offer
|
||||||
|
LocalOfferSent --> ClosePending: local_sig_received
|
||||||
|
}
|
||||||
|
|
||||||
|
state RemoteSide {
|
||||||
|
[*] --> RemoteCloseStart
|
||||||
|
RemoteCloseStart --> ClosePending: offer_received
|
||||||
|
}
|
||||||
|
|
||||||
|
CloseErr --> LocalCloseStart: send_offer_rbf
|
||||||
|
CloseErr --> RemoteCloseStart: offer_received_rbf
|
||||||
|
LocalCloseStart --> CloseErr: protocol_violation
|
||||||
|
RemoteCloseStart --> CloseErr: protocol_violation
|
||||||
|
|
||||||
|
ClosePending --> LocalCloseStart: send_offer_rbf
|
||||||
|
ClosePending --> RemoteCloseStart: offer_received_rbf
|
||||||
|
}
|
||||||
|
|
||||||
|
ClosingNegotiation --> CloseFin: txn_confirmation
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ChannelActive
|
||||||
|
|
||||||
|
The initial state where the channel is fully operational.
|
||||||
|
|
||||||
|
- **Transitions**:
|
||||||
|
- `send_shutdown` → `ShutdownPending` (Local node initiates closing process)
|
||||||
|
- `shutdown_received` → `ShutdownPending` (Remote node initiates closing process)
|
||||||
|
|
||||||
|
### ShutdownPending
|
||||||
|
|
||||||
|
The channel has entered the closing process but is still processing existing HTLCs.
|
||||||
|
|
||||||
|
- **Transitions**:
|
||||||
|
- `shutdown_received` → `ChannelFlushing` (When both parties have exchanged
|
||||||
|
shutdown messages)
|
||||||
|
|
||||||
|
- `shutdown_complete` → `ChannelFlushing` (When local shutdown processing is
|
||||||
|
complete)
|
||||||
|
|
||||||
|
### ChannelFlushing
|
||||||
|
|
||||||
|
The channel no longer accepts new HTLCs and is waiting for existing HTLCs to
|
||||||
|
resolve.
|
||||||
|
|
||||||
|
- **Transitions**:
|
||||||
|
- `channel_flushed` → `ClosingNegotiation` (When all HTLCs are settled)
|
||||||
|
|
||||||
|
### ClosingNegotiation
|
||||||
|
This composite state encompasses multiple substates related to the negotiation
|
||||||
|
|
||||||
|
of the closing transaction fee. It's divided into local and remote operation
|
||||||
|
flows.
|
||||||
|
|
||||||
|
#### LocalSide
|
||||||
|
|
||||||
|
- **LocalCloseStart**: Ready to initiate or respond to closing negotiation from
|
||||||
|
local perspective.
|
||||||
|
- **Transitions**:
|
||||||
|
- `send_offer` → `LocalOfferSent` (Local node proposes a closing fee)
|
||||||
|
- `protocol_violation` → `CloseErr` (Invalid message or state detected)
|
||||||
|
|
||||||
|
- **LocalOfferSent**: Local node has sent a closing offer and is awaiting
|
||||||
|
confirmation.
|
||||||
|
- **Transitions**:
|
||||||
|
- `local_sig_received` → `ClosePending` (Local signature validated and
|
||||||
|
accepted)
|
||||||
|
|
||||||
|
#### RemoteSide
|
||||||
|
|
||||||
|
- **RemoteCloseStart**: Ready to process closing negotiation initiated by remote
|
||||||
|
node.
|
||||||
|
- **Transitions**:
|
||||||
|
- `offer_received` → `ClosePending` (Remote node proposal received and
|
||||||
|
accepted)
|
||||||
|
- `protocol_violation` → `CloseErr` (Invalid message or state detected)
|
||||||
|
|
||||||
|
#### Shared States
|
||||||
|
|
||||||
|
- **ClosePending**: Closing transaction has been negotiated, signed by both
|
||||||
|
parties, and broadcast to the network. Waiting for on-chain confirmation.
|
||||||
|
|
||||||
|
- **Transitions**:
|
||||||
|
- `send_offer_rbf` → `LocalCloseStart` (Local node initiates fee increase
|
||||||
|
via RBF)
|
||||||
|
|
||||||
|
- `offer_received_rbf` → `RemoteCloseStart` (Remote node initiates fee
|
||||||
|
increase via RBF)
|
||||||
|
|
||||||
|
- **CloseErr**: Error state for handling protocol violations.
|
||||||
|
- **Transitions**:
|
||||||
|
- `send_offer_rbf` → `LocalCloseStart` (Local node attempts recovery with
|
||||||
|
new offer)
|
||||||
|
|
||||||
|
- `offer_received_rbf` → `RemoteCloseStart` (Remote node attempts recovery
|
||||||
|
with new offer)
|
||||||
|
|
||||||
|
### CloseFin
|
||||||
|
|
||||||
|
The closing transaction has been confirmed on the blockchain, and the channel is
|
||||||
|
considered closed.
|
||||||
|
|
||||||
|
- **Transitions**: None (terminal state)
|
||||||
|
|
||||||
|
## Fee Negotiation Process
|
||||||
|
|
||||||
|
Fee negotiation is a critical component of the cooperative close process. The
|
||||||
|
protocol supports Replace-By-Fee (RBF) to handle changing network fee
|
||||||
|
conditions:
|
||||||
|
|
||||||
|
1. Initial fee proposal is based on current network fee estimation
|
||||||
|
2. Either party can propose higher fees using RBF if confirmation is taking too
|
||||||
|
long
|
||||||
|
3. Nodes can alternate proposals until reaching agreement
|
||||||
|
4. Each proposal must satisfy RBF requirements (incremental fee increase)
|
||||||
|
|
||||||
|
## Protocol Violation Handling
|
||||||
|
|
||||||
|
The `CloseErr` state provides recovery paths when protocol violations occur:
|
||||||
|
|
||||||
|
- Invalid signature formats
|
||||||
|
- Out-of-sequence messages
|
||||||
|
- Timeout violations
|
||||||
|
- Malformed closing transactions
|
||||||
|
|
||||||
|
Recovery typically involves restarting the negotiation with a new closing offer.
|
||||||
|
|
||||||
|
## Example Scenarios
|
||||||
|
|
||||||
|
### Standard Cooperative Close
|
||||||
|
|
||||||
|
1. Local node initiates closing: `ChannelActive` → `ShutdownPending` (via
|
||||||
|
`send_shutdown`)
|
||||||
|
|
||||||
|
2. Remote acknowledges: `ShutdownPending` → `ChannelFlushing` (via
|
||||||
|
`shutdown_received`)
|
||||||
|
|
||||||
|
3. HTLCs resolve: `ChannelFlushing` → `ClosingNegotiation` (via
|
||||||
|
`channel_flushed`)
|
||||||
|
|
||||||
|
4. Local proposes fee: `LocalCloseStart` → `LocalOfferSent` (via `send_offer`)
|
||||||
|
|
||||||
|
5. Local signature validated: `LocalOfferSent` → `ClosePending` (via
|
||||||
|
`local_sig_received`)
|
||||||
|
|
||||||
|
6. Transaction confirms: `ClosePending` → `CloseFin` (via `txn_confirmation`)
|
||||||
|
|
||||||
|
### Fee Bump Due to Network Congestion
|
||||||
|
|
||||||
|
1. ... (steps 1-5 same as above)
|
||||||
|
2. While in `ClosePending`, network fees increase
|
||||||
|
|
||||||
|
3. Local node initiates RBF: `ClosePending` → `LocalCloseStart` (via
|
||||||
|
`send_offer_rbf`)
|
||||||
|
|
||||||
|
4. New proposal cycle begins with higher fees
|
||||||
|
|
||||||
|
5. When agreement is reached on new fees: `ClosePending` → `CloseFin` (via
|
||||||
|
`txn_confirmation`)
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
- This state machine is implemented in the `peer.go` and `channel.go` files
|
||||||
|
within the lnd codebase
|
||||||
|
- State transitions are logged at the debug level
|
||||||
|
- The `ChanCloser` interface manages the state machine execution
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
package chancloser
|
package chancloser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
"github.com/lightningnetwork/lnd/fn/v2"
|
"github.com/lightningnetwork/lnd/fn/v2"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
"github.com/lightningnetwork/lnd/msgmux"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RbfMsgMapper is a struct that implements the MsgMapper interface for the
|
// RbfMsgMapper is a struct that implements the MsgMapper interface for the
|
||||||
@@ -16,16 +18,21 @@ type RbfMsgMapper struct {
|
|||||||
|
|
||||||
// chanID is the channel ID of the channel being closed.
|
// chanID is the channel ID of the channel being closed.
|
||||||
chanID lnwire.ChannelID
|
chanID lnwire.ChannelID
|
||||||
|
|
||||||
|
// peerPub is the public key of the peer that the channel is being
|
||||||
|
// closed.
|
||||||
|
peerPub btcec.PublicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRbfMsgMapper creates a new RbfMsgMapper instance given the current block
|
// NewRbfMsgMapper creates a new RbfMsgMapper instance given the current block
|
||||||
// height when the co-op close request was initiated.
|
// height when the co-op close request was initiated.
|
||||||
func NewRbfMsgMapper(blockHeight uint32,
|
func NewRbfMsgMapper(blockHeight uint32,
|
||||||
chanID lnwire.ChannelID) *RbfMsgMapper {
|
chanID lnwire.ChannelID, peerPub btcec.PublicKey) *RbfMsgMapper {
|
||||||
|
|
||||||
return &RbfMsgMapper{
|
return &RbfMsgMapper{
|
||||||
blockHeight: blockHeight,
|
blockHeight: blockHeight,
|
||||||
chanID: chanID,
|
chanID: chanID,
|
||||||
|
peerPub: peerPub,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,18 +41,20 @@ func someEvent[T ProtocolEvent](m T) fn.Option[ProtocolEvent] {
|
|||||||
return fn.Some(ProtocolEvent(m))
|
return fn.Some(ProtocolEvent(m))
|
||||||
}
|
}
|
||||||
|
|
||||||
// isExpectedChanID returns true if the channel ID of the message matches the
|
// isForUs returns true if the channel ID + pubkey of the message matches the
|
||||||
// bound instance.
|
// bound instance.
|
||||||
func (r *RbfMsgMapper) isExpectedChanID(chanID lnwire.ChannelID) bool {
|
func (r *RbfMsgMapper) isForUs(chanID lnwire.ChannelID,
|
||||||
return r.chanID == chanID
|
fromPub btcec.PublicKey) bool {
|
||||||
|
|
||||||
|
return r.chanID == chanID && r.peerPub.IsEqual(&fromPub)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MapMsg maps a wire message into a FSM event. If the message is not mappable,
|
// MapMsg maps a wire message into a FSM event. If the message is not mappable,
|
||||||
// then an error is returned.
|
// then an error is returned.
|
||||||
func (r *RbfMsgMapper) MapMsg(wireMsg lnwire.Message) fn.Option[ProtocolEvent] {
|
func (r *RbfMsgMapper) MapMsg(wireMsg msgmux.PeerMsg) fn.Option[ProtocolEvent] {
|
||||||
switch msg := wireMsg.(type) {
|
switch msg := wireMsg.Message.(type) {
|
||||||
case *lnwire.Shutdown:
|
case *lnwire.Shutdown:
|
||||||
if !r.isExpectedChanID(msg.ChannelID) {
|
if !r.isForUs(msg.ChannelID, wireMsg.PeerPub) {
|
||||||
return fn.None[ProtocolEvent]()
|
return fn.None[ProtocolEvent]()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +64,7 @@ func (r *RbfMsgMapper) MapMsg(wireMsg lnwire.Message) fn.Option[ProtocolEvent] {
|
|||||||
})
|
})
|
||||||
|
|
||||||
case *lnwire.ClosingComplete:
|
case *lnwire.ClosingComplete:
|
||||||
if !r.isExpectedChanID(msg.ChannelID) {
|
if !r.isForUs(msg.ChannelID, wireMsg.PeerPub) {
|
||||||
return fn.None[ProtocolEvent]()
|
return fn.None[ProtocolEvent]()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +73,7 @@ func (r *RbfMsgMapper) MapMsg(wireMsg lnwire.Message) fn.Option[ProtocolEvent] {
|
|||||||
})
|
})
|
||||||
|
|
||||||
case *lnwire.ClosingSig:
|
case *lnwire.ClosingSig:
|
||||||
if !r.isExpectedChanID(msg.ChannelID) {
|
if !r.isForUs(msg.ChannelID, wireMsg.PeerPub) {
|
||||||
return fn.None[ProtocolEvent]()
|
return fn.None[ProtocolEvent]()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,11 @@ var (
|
|||||||
// ErrCloserAndClosee is returned when we expect a sig covering both
|
// ErrCloserAndClosee is returned when we expect a sig covering both
|
||||||
// outputs, it isn't present.
|
// outputs, it isn't present.
|
||||||
ErrCloserAndClosee = fmt.Errorf("expected CloserAndClosee sig")
|
ErrCloserAndClosee = fmt.Errorf("expected CloserAndClosee sig")
|
||||||
|
|
||||||
|
// ErrWrongLocalScript is returned when the remote party sends a
|
||||||
|
// ClosingComplete message that doesn't carry our last local script
|
||||||
|
// sent.
|
||||||
|
ErrWrongLocalScript = fmt.Errorf("wrong local script")
|
||||||
)
|
)
|
||||||
|
|
||||||
// ProtocolEvent is a special interface used to create the equivalent of a
|
// ProtocolEvent is a special interface used to create the equivalent of a
|
||||||
@@ -361,6 +366,9 @@ type ProtocolState interface {
|
|||||||
// ProcessEvent takes a protocol event, and implements a state
|
// ProcessEvent takes a protocol event, and implements a state
|
||||||
// transition for the state.
|
// transition for the state.
|
||||||
ProcessEvent(ProtocolEvent, *Environment) (*CloseStateTransition, error)
|
ProcessEvent(ProtocolEvent, *Environment) (*CloseStateTransition, error)
|
||||||
|
|
||||||
|
// String returns the name of the state.
|
||||||
|
String() string
|
||||||
}
|
}
|
||||||
|
|
||||||
// AsymmetricPeerState is an extension of the normal ProtocolState interface
|
// AsymmetricPeerState is an extension of the normal ProtocolState interface
|
||||||
@@ -379,7 +387,7 @@ type AsymmetricPeerState interface {
|
|||||||
type ProtocolStates interface {
|
type ProtocolStates interface {
|
||||||
ChannelActive | ShutdownPending | ChannelFlushing | ClosingNegotiation |
|
ChannelActive | ShutdownPending | ChannelFlushing | ClosingNegotiation |
|
||||||
LocalCloseStart | LocalOfferSent | RemoteCloseStart |
|
LocalCloseStart | LocalOfferSent | RemoteCloseStart |
|
||||||
ClosePending | CloseFin
|
ClosePending | CloseFin | CloseErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChannelActive is the base state for the channel closer state machine. In
|
// ChannelActive is the base state for the channel closer state machine. In
|
||||||
@@ -401,6 +409,11 @@ type ProtocolStates interface {
|
|||||||
type ChannelActive struct {
|
type ChannelActive struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// String returns the name of the state for ChannelActive.
|
||||||
|
func (c *ChannelActive) String() string {
|
||||||
|
return "ChannelActive"
|
||||||
|
}
|
||||||
|
|
||||||
// IsTerminal returns true if the target state is a terminal state.
|
// IsTerminal returns true if the target state is a terminal state.
|
||||||
func (c *ChannelActive) IsTerminal() bool {
|
func (c *ChannelActive) IsTerminal() bool {
|
||||||
return false
|
return false
|
||||||
@@ -441,6 +454,16 @@ type ShutdownPending struct {
|
|||||||
// IdealFeeRate is the ideal fee rate we'd like to use for the closing
|
// IdealFeeRate is the ideal fee rate we'd like to use for the closing
|
||||||
// attempt.
|
// attempt.
|
||||||
IdealFeeRate fn.Option[chainfee.SatPerVByte]
|
IdealFeeRate fn.Option[chainfee.SatPerVByte]
|
||||||
|
|
||||||
|
// EarlyRemoteOffer is the offer we received from the remote party
|
||||||
|
// before we received their shutdown message. We'll stash it to process
|
||||||
|
// later.
|
||||||
|
EarlyRemoteOffer fn.Option[OfferReceivedEvent]
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the name of the state for ShutdownPending.
|
||||||
|
func (s *ShutdownPending) String() string {
|
||||||
|
return "ShutdownPending"
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsTerminal returns true if the target state is a terminal state.
|
// IsTerminal returns true if the target state is a terminal state.
|
||||||
@@ -478,6 +501,11 @@ type ChannelFlushing struct {
|
|||||||
IdealFeeRate fn.Option[chainfee.SatPerVByte]
|
IdealFeeRate fn.Option[chainfee.SatPerVByte]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// String returns the name of the state for ChannelFlushing.
|
||||||
|
func (c *ChannelFlushing) String() string {
|
||||||
|
return "ChannelFlushing"
|
||||||
|
}
|
||||||
|
|
||||||
// protocolStateSealed indicates that this struct is a ProtocolEvent instance.
|
// protocolStateSealed indicates that this struct is a ProtocolEvent instance.
|
||||||
func (c *ChannelFlushing) protocolStateSealed() {}
|
func (c *ChannelFlushing) protocolStateSealed() {}
|
||||||
|
|
||||||
@@ -505,6 +533,22 @@ type ClosingNegotiation struct {
|
|||||||
// the ShouldRouteTo method to determine which state route incoming
|
// the ShouldRouteTo method to determine which state route incoming
|
||||||
// events to.
|
// events to.
|
||||||
PeerState lntypes.Dual[AsymmetricPeerState]
|
PeerState lntypes.Dual[AsymmetricPeerState]
|
||||||
|
|
||||||
|
// CloseChannelTerms is the terms we'll use to close the channel. We
|
||||||
|
// hold a value here which is pointed to by the various
|
||||||
|
// AsymmetricPeerState instances. This allows us to update this value if
|
||||||
|
// the remote peer sends a new address, with each of the state noting
|
||||||
|
// the new value via a pointer.
|
||||||
|
*CloseChannelTerms
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the name of the state for ClosingNegotiation.
|
||||||
|
func (c *ClosingNegotiation) String() string {
|
||||||
|
localState := c.PeerState.GetForParty(lntypes.Local)
|
||||||
|
remoteState := c.PeerState.GetForParty(lntypes.Remote)
|
||||||
|
|
||||||
|
return fmt.Sprintf("ClosingNegotiation(local=%v, remote=%v)",
|
||||||
|
localState, remoteState)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsTerminal returns true if the target state is a terminal state.
|
// IsTerminal returns true if the target state is a terminal state.
|
||||||
@@ -515,6 +559,56 @@ func (c *ClosingNegotiation) IsTerminal() bool {
|
|||||||
// protocolSealed indicates that this struct is a ProtocolEvent instance.
|
// protocolSealed indicates that this struct is a ProtocolEvent instance.
|
||||||
func (c *ClosingNegotiation) protocolStateSealed() {}
|
func (c *ClosingNegotiation) protocolStateSealed() {}
|
||||||
|
|
||||||
|
// ErrState can be used to introspect into a benign error related to a state
|
||||||
|
// transition.
|
||||||
|
type ErrState interface {
|
||||||
|
sealed()
|
||||||
|
|
||||||
|
error
|
||||||
|
|
||||||
|
// Err returns an error for the ErrState.
|
||||||
|
Err() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrStateCantPayForFee is sent when the local party attempts a fee update
|
||||||
|
// that they can't actually party for.
|
||||||
|
type ErrStateCantPayForFee struct {
|
||||||
|
localBalance btcutil.Amount
|
||||||
|
|
||||||
|
attemptedFee btcutil.Amount
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewErrStateCantPayForFee returns a new NewErrStateCantPayForFee error.
|
||||||
|
func NewErrStateCantPayForFee(localBalance, attemptedFee btcutil.Amount,
|
||||||
|
) *ErrStateCantPayForFee {
|
||||||
|
|
||||||
|
return &ErrStateCantPayForFee{
|
||||||
|
localBalance: localBalance,
|
||||||
|
attemptedFee: attemptedFee,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sealed makes this a sealed interface.
|
||||||
|
func (e *ErrStateCantPayForFee) sealed() {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Err returns an error for the ErrState.
|
||||||
|
func (e *ErrStateCantPayForFee) Err() error {
|
||||||
|
return fmt.Errorf("cannot pay for fee of %v, only have %v local "+
|
||||||
|
"balance", e.attemptedFee, e.localBalance)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error returns the error string for the ErrState.
|
||||||
|
func (e *ErrStateCantPayForFee) Error() string {
|
||||||
|
return e.Err().Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the string for the ErrStateCantPayForFee.
|
||||||
|
func (e *ErrStateCantPayForFee) String() string {
|
||||||
|
return fmt.Sprintf("ErrStateCantPayForFee(local_balance=%v, "+
|
||||||
|
"attempted_fee=%v)", e.localBalance, e.attemptedFee)
|
||||||
|
}
|
||||||
|
|
||||||
// CloseChannelTerms is a set of terms that we'll use to close the channel. This
|
// CloseChannelTerms is a set of terms that we'll use to close the channel. This
|
||||||
// includes the balances of the channel, and the scripts we'll use to send each
|
// includes the balances of the channel, and the scripts we'll use to send each
|
||||||
// party's funds to.
|
// party's funds to.
|
||||||
@@ -526,11 +620,11 @@ type CloseChannelTerms struct {
|
|||||||
|
|
||||||
// DeriveCloseTxOuts takes the close terms, and returns the local and remote tx
|
// DeriveCloseTxOuts takes the close terms, and returns the local and remote tx
|
||||||
// out for the close transaction. If an output is dust, then it'll be nil.
|
// out for the close transaction. If an output is dust, then it'll be nil.
|
||||||
//
|
|
||||||
// TODO(roasbeef): add func for w/e heuristic to not manifest own output?
|
|
||||||
func (c *CloseChannelTerms) DeriveCloseTxOuts() (*wire.TxOut, *wire.TxOut) {
|
func (c *CloseChannelTerms) DeriveCloseTxOuts() (*wire.TxOut, *wire.TxOut) {
|
||||||
//nolint:ll
|
//nolint:ll
|
||||||
deriveTxOut := func(balance btcutil.Amount, pkScript []byte) *wire.TxOut {
|
deriveTxOut := func(balance btcutil.Amount, pkScript []byte) *wire.TxOut {
|
||||||
|
// We'll base the existence of the output on our normal dust
|
||||||
|
// check.
|
||||||
dustLimit := lnwallet.DustLimitForSize(len(pkScript))
|
dustLimit := lnwallet.DustLimitForSize(len(pkScript))
|
||||||
if balance >= dustLimit {
|
if balance >= dustLimit {
|
||||||
return &wire.TxOut{
|
return &wire.TxOut{
|
||||||
@@ -591,7 +685,13 @@ func (c *CloseChannelTerms) RemoteCanPayFees(absoluteFee btcutil.Amount) bool {
|
|||||||
// input events:
|
// input events:
|
||||||
// - SendOfferEvent
|
// - SendOfferEvent
|
||||||
type LocalCloseStart struct {
|
type LocalCloseStart struct {
|
||||||
CloseChannelTerms
|
*CloseChannelTerms
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the name of the state for LocalCloseStart, including proposed
|
||||||
|
// fee details.
|
||||||
|
func (l *LocalCloseStart) String() string {
|
||||||
|
return "LocalCloseStart"
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShouldRouteTo returns true if the target state should process the target
|
// ShouldRouteTo returns true if the target state should process the target
|
||||||
@@ -625,15 +725,23 @@ func (l *LocalCloseStart) protocolStateSealed() {}
|
|||||||
// input events:
|
// input events:
|
||||||
// - LocalSigReceived
|
// - LocalSigReceived
|
||||||
type LocalOfferSent struct {
|
type LocalOfferSent struct {
|
||||||
CloseChannelTerms
|
*CloseChannelTerms
|
||||||
|
|
||||||
// ProposedFee is the fee we proposed to the remote party.
|
// ProposedFee is the fee we proposed to the remote party.
|
||||||
ProposedFee btcutil.Amount
|
ProposedFee btcutil.Amount
|
||||||
|
|
||||||
|
// ProposedFeeRate is the fee rate we proposed to the remote party.
|
||||||
|
ProposedFeeRate chainfee.SatPerVByte
|
||||||
|
|
||||||
// LocalSig is the signature we sent to the remote party.
|
// LocalSig is the signature we sent to the remote party.
|
||||||
LocalSig lnwire.Sig
|
LocalSig lnwire.Sig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// String returns the name of the state for LocalOfferSent, including proposed.
|
||||||
|
func (l *LocalOfferSent) String() string {
|
||||||
|
return fmt.Sprintf("LocalOfferSent(proposed_fee=%v)", l.ProposedFee)
|
||||||
|
}
|
||||||
|
|
||||||
// ShouldRouteTo returns true if the target state should process the target
|
// ShouldRouteTo returns true if the target state should process the target
|
||||||
// event.
|
// event.
|
||||||
func (l *LocalOfferSent) ShouldRouteTo(event ProtocolEvent) bool {
|
func (l *LocalOfferSent) ShouldRouteTo(event ProtocolEvent) bool {
|
||||||
@@ -668,6 +776,27 @@ func (l *LocalOfferSent) IsTerminal() bool {
|
|||||||
type ClosePending struct {
|
type ClosePending struct {
|
||||||
// CloseTx is the pending close transaction.
|
// CloseTx is the pending close transaction.
|
||||||
CloseTx *wire.MsgTx
|
CloseTx *wire.MsgTx
|
||||||
|
|
||||||
|
*CloseChannelTerms
|
||||||
|
|
||||||
|
// FeeRate is the fee rate of the closing transaction.
|
||||||
|
FeeRate chainfee.SatPerVByte
|
||||||
|
|
||||||
|
// Party indicates which party is at this state. This is used to
|
||||||
|
// implement the state transition properly, based on ShouldRouteTo.
|
||||||
|
Party lntypes.ChannelParty
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the name of the state for ClosePending.
|
||||||
|
func (c *ClosePending) String() string {
|
||||||
|
return fmt.Sprintf("ClosePending(txid=%v, party=%v, fee_rate=%v)",
|
||||||
|
c.CloseTx.TxHash(), c.Party, c.FeeRate)
|
||||||
|
}
|
||||||
|
|
||||||
|
// isType returns true if the value is of type T.
|
||||||
|
func isType[T any](value any) bool {
|
||||||
|
_, ok := value.(T)
|
||||||
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShouldRouteTo returns true if the target state should process the target
|
// ShouldRouteTo returns true if the target state should process the target
|
||||||
@@ -677,6 +806,17 @@ func (c *ClosePending) ShouldRouteTo(event ProtocolEvent) bool {
|
|||||||
case *SpendEvent:
|
case *SpendEvent:
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
|
switch {
|
||||||
|
case c.Party == lntypes.Local && isType[*SendOfferEvent](event):
|
||||||
|
return true
|
||||||
|
|
||||||
|
case c.Party == lntypes.Remote && isType[*OfferReceivedEvent](
|
||||||
|
event,
|
||||||
|
):
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -696,6 +836,11 @@ type CloseFin struct {
|
|||||||
ConfirmedTx *wire.MsgTx
|
ConfirmedTx *wire.MsgTx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// String returns the name of the state for CloseFin.
|
||||||
|
func (c *CloseFin) String() string {
|
||||||
|
return "CloseFin"
|
||||||
|
}
|
||||||
|
|
||||||
// protocolStateSealed indicates that this struct is a ProtocolEvent instance.
|
// protocolStateSealed indicates that this struct is a ProtocolEvent instance.
|
||||||
func (c *CloseFin) protocolStateSealed() {}
|
func (c *CloseFin) protocolStateSealed() {}
|
||||||
|
|
||||||
@@ -711,7 +856,12 @@ func (c *CloseFin) IsTerminal() bool {
|
|||||||
// - fromState: ChannelFlushing
|
// - fromState: ChannelFlushing
|
||||||
// - toState: ClosePending
|
// - toState: ClosePending
|
||||||
type RemoteCloseStart struct {
|
type RemoteCloseStart struct {
|
||||||
CloseChannelTerms
|
*CloseChannelTerms
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the name of the state for RemoteCloseStart.
|
||||||
|
func (r *RemoteCloseStart) String() string {
|
||||||
|
return "RemoteCloseStart"
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShouldRouteTo returns true if the target state should process the target
|
// ShouldRouteTo returns true if the target state should process the target
|
||||||
@@ -733,6 +883,54 @@ func (l *RemoteCloseStart) IsTerminal() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CloseErr is an error state in the protocol. We enter this state when a
|
||||||
|
// protocol constraint is violated, or an upfront sanity check fails.
|
||||||
|
type CloseErr struct {
|
||||||
|
ErrState
|
||||||
|
|
||||||
|
*CloseChannelTerms
|
||||||
|
|
||||||
|
// Party indicates which party is at this state. This is used to
|
||||||
|
// implement the state transition properly, based on ShouldRouteTo.
|
||||||
|
Party lntypes.ChannelParty
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the name of the state for CloseErr, including error and party
|
||||||
|
// details.
|
||||||
|
func (c *CloseErr) String() string {
|
||||||
|
return fmt.Sprintf("CloseErr(party=%v, err=%v)", c.Party, c.ErrState)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShouldRouteTo returns true if the target state should process the target
|
||||||
|
// event.
|
||||||
|
func (c *CloseErr) ShouldRouteTo(event ProtocolEvent) bool {
|
||||||
|
switch event.(type) {
|
||||||
|
case *SpendEvent:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
switch {
|
||||||
|
case c.Party == lntypes.Local && isType[*SendOfferEvent](event):
|
||||||
|
return true
|
||||||
|
|
||||||
|
case c.Party == lntypes.Remote && isType[*OfferReceivedEvent](
|
||||||
|
event,
|
||||||
|
):
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// protocolStateSealed indicates that this struct is a ProtocolEvent instance.
|
||||||
|
func (c *CloseErr) protocolStateSealed() {}
|
||||||
|
|
||||||
|
// IsTerminal returns true if the target state is a terminal state.
|
||||||
|
func (c *CloseErr) IsTerminal() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// RbfChanCloser is a state machine that handles the RBF-enabled cooperative
|
// RbfChanCloser is a state machine that handles the RBF-enabled cooperative
|
||||||
// channel close protocol.
|
// channel close protocol.
|
||||||
type RbfChanCloser = protofsm.StateMachine[ProtocolEvent, *Environment]
|
type RbfChanCloser = protofsm.StateMachine[ProtocolEvent, *Environment]
|
||||||
@@ -761,3 +959,7 @@ type RbfState = protofsm.State[ProtocolEvent, *Environment]
|
|||||||
|
|
||||||
// RbfEvent is a type alias for the event type of the RBF channel closer.
|
// RbfEvent is a type alias for the event type of the RBF channel closer.
|
||||||
type RbfEvent = protofsm.EmittedEvent[ProtocolEvent]
|
type RbfEvent = protofsm.EmittedEvent[ProtocolEvent]
|
||||||
|
|
||||||
|
// RbfStateSub is a type alias for the state subscription type of the RBF chan
|
||||||
|
// closer.
|
||||||
|
type RbfStateSub = protofsm.StateSubscriber[ProtocolEvent, *Environment]
|
||||||
@@ -110,7 +110,10 @@ func assertStateTransitions[Event any, Env protofsm.Environment](
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
for _, expectedState := range expectedStates {
|
for _, expectedState := range expectedStates {
|
||||||
newState := <-stateSub.NewItemCreated.ChanOut()
|
newState, err := fn.RecvOrTimeout(
|
||||||
|
stateSub.NewItemCreated.ChanOut(), 10*time.Millisecond,
|
||||||
|
)
|
||||||
|
require.NoError(t, err, "expected state: %T", expectedState)
|
||||||
|
|
||||||
require.IsType(t, expectedState, newState)
|
require.IsType(t, expectedState, newState)
|
||||||
}
|
}
|
||||||
@@ -154,6 +157,27 @@ func assertUnknownEventFail(t *testing.T, startingState ProtocolState) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// assertSpendEventCloseFin asserts that the state machine transitions to the
|
||||||
|
// CloseFin state when a spend event is received.
|
||||||
|
func assertSpendEventCloseFin(t *testing.T, startingState ProtocolState) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
// If a spend event is received, the state machine should transition to
|
||||||
|
// the CloseFin state.
|
||||||
|
t.Run("spend_event", func(t *testing.T) {
|
||||||
|
closeHarness := newCloser(t, &harnessCfg{
|
||||||
|
initialState: fn.Some(startingState),
|
||||||
|
})
|
||||||
|
defer closeHarness.stopAndAssert()
|
||||||
|
|
||||||
|
closeHarness.chanCloser.SendEvent(
|
||||||
|
context.Background(), &SpendEvent{},
|
||||||
|
)
|
||||||
|
|
||||||
|
closeHarness.assertStateTransitions(&CloseFin{})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
type harnessCfg struct {
|
type harnessCfg struct {
|
||||||
initialState fn.Option[ProtocolState]
|
initialState fn.Option[ProtocolState]
|
||||||
|
|
||||||
@@ -248,6 +272,8 @@ func (r *rbfCloserTestHarness) assertNoStateTransitions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *rbfCloserTestHarness) assertStateTransitions(states ...RbfState) {
|
func (r *rbfCloserTestHarness) assertStateTransitions(states ...RbfState) {
|
||||||
|
r.T.Helper()
|
||||||
|
|
||||||
assertStateTransitions(r.T, r.stateSub, states)
|
assertStateTransitions(r.T, r.stateSub, states)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -598,6 +624,8 @@ func (r *rbfCloserTestHarness) assertSingleRbfIteration(
|
|||||||
// response of the remote party, which completes one iteration
|
// response of the remote party, which completes one iteration
|
||||||
localSigEvent := &LocalSigReceived{
|
localSigEvent := &LocalSigReceived{
|
||||||
SigMsg: lnwire.ClosingSig{
|
SigMsg: lnwire.ClosingSig{
|
||||||
|
CloserScript: localAddr,
|
||||||
|
CloseeScript: remoteAddr,
|
||||||
ClosingSigs: lnwire.ClosingSigs{
|
ClosingSigs: lnwire.ClosingSigs{
|
||||||
CloserAndClosee: newSigTlv[tlv.TlvType3](
|
CloserAndClosee: newSigTlv[tlv.TlvType3](
|
||||||
remoteWireSig,
|
remoteWireSig,
|
||||||
@@ -620,34 +648,34 @@ func (r *rbfCloserTestHarness) assertSingleRbfIteration(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *rbfCloserTestHarness) assertSingleRemoteRbfIteration(
|
func (r *rbfCloserTestHarness) assertSingleRemoteRbfIteration(
|
||||||
initEvent ProtocolEvent, balanceAfterClose, absoluteFee btcutil.Amount,
|
initEvent *OfferReceivedEvent, balanceAfterClose,
|
||||||
sequence uint32, iteration bool) {
|
absoluteFee btcutil.Amount, sequence uint32, iteration bool,
|
||||||
|
sendInit bool) {
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// If this is an iteration, then we expect some intermediate states,
|
|
||||||
// before we enter the main RBF/sign loop.
|
|
||||||
if iteration {
|
|
||||||
r.expectFeeEstimate(absoluteFee, 1)
|
|
||||||
|
|
||||||
r.assertStateTransitions(
|
|
||||||
&ChannelActive{}, &ShutdownPending{},
|
|
||||||
&ChannelFlushing{}, &ClosingNegotiation{},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// When we receive the signature below, our local state machine should
|
// When we receive the signature below, our local state machine should
|
||||||
// move to finalize the close.
|
// move to finalize the close.
|
||||||
r.expectRemoteCloseFinalized(
|
r.expectRemoteCloseFinalized(
|
||||||
&localSig, &remoteSig, localAddr, remoteAddr,
|
&localSig, &remoteSig, initEvent.SigMsg.CloseeScript,
|
||||||
|
initEvent.SigMsg.CloserScript,
|
||||||
absoluteFee, balanceAfterClose, false,
|
absoluteFee, balanceAfterClose, false,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if sendInit {
|
||||||
r.chanCloser.SendEvent(ctx, initEvent)
|
r.chanCloser.SendEvent(ctx, initEvent)
|
||||||
|
}
|
||||||
|
|
||||||
// Our outer state should transition to ClosingNegotiation state.
|
// Our outer state should transition to ClosingNegotiation state.
|
||||||
r.assertStateTransitions(&ClosingNegotiation{})
|
r.assertStateTransitions(&ClosingNegotiation{})
|
||||||
|
|
||||||
|
// If this is an iteration, then we'll go from ClosePending ->
|
||||||
|
// RemoteCloseStart -> ClosePending. So we'll assert an extra transition
|
||||||
|
// here.
|
||||||
|
if iteration {
|
||||||
|
r.assertStateTransitions(&ClosingNegotiation{})
|
||||||
|
}
|
||||||
|
|
||||||
// If we examine the final resting state, we should see that the we're
|
// If we examine the final resting state, we should see that the we're
|
||||||
// now in the ClosePending state for the remote peer.
|
// now in the ClosePending state for the remote peer.
|
||||||
currentState := assertStateT[*ClosingNegotiation](r)
|
currentState := assertStateT[*ClosingNegotiation](r)
|
||||||
@@ -687,7 +715,7 @@ func newRbfCloserTestHarness(t *testing.T,
|
|||||||
|
|
||||||
peerPub := randPubKey(t)
|
peerPub := randPubKey(t)
|
||||||
|
|
||||||
msgMapper := NewRbfMsgMapper(uint32(startingHeight), chanID)
|
msgMapper := NewRbfMsgMapper(uint32(startingHeight), chanID, *peerPub)
|
||||||
|
|
||||||
initialState := cfg.initialState.UnwrapOr(&ChannelActive{})
|
initialState := cfg.initialState.UnwrapOr(&ChannelActive{})
|
||||||
|
|
||||||
@@ -858,7 +886,28 @@ func TestRbfChannelActiveTransitions(t *testing.T) {
|
|||||||
closeHarness.waitForMsgSent()
|
closeHarness.waitForMsgSent()
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO(roasbeef): thaw height fail
|
// If the remote party attempts to close, and a thaw height is active,
|
||||||
|
// but not yet met, then we should fail.
|
||||||
|
t.Run("remote_initiated_thaw_height_close_fail", func(t *testing.T) {
|
||||||
|
closeHarness := newCloser(t, &harnessCfg{
|
||||||
|
localUpfrontAddr: fn.Some(localAddr),
|
||||||
|
thawHeight: fn.Some(uint32(100000)),
|
||||||
|
})
|
||||||
|
defer closeHarness.stopAndAssert()
|
||||||
|
|
||||||
|
// Next, we'll emit the recv event, with the addr of the remote
|
||||||
|
// party.
|
||||||
|
closeHarness.chanCloser.SendEvent(
|
||||||
|
ctx, &ShutdownReceived{
|
||||||
|
ShutdownScript: remoteAddr,
|
||||||
|
BlockHeight: 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// We expect a failure as the block height is less than the
|
||||||
|
// start height.
|
||||||
|
closeHarness.expectFailure(ErrThawHeightNotReached)
|
||||||
|
})
|
||||||
|
|
||||||
// When we receive a shutdown, we should transition to the shutdown
|
// When we receive a shutdown, we should transition to the shutdown
|
||||||
// pending state, with the local+remote shutdown addrs known.
|
// pending state, with the local+remote shutdown addrs known.
|
||||||
@@ -902,6 +951,9 @@ func TestRbfChannelActiveTransitions(t *testing.T) {
|
|||||||
|
|
||||||
// Any other event should be ignored.
|
// Any other event should be ignored.
|
||||||
assertUnknownEventFail(t, &ChannelActive{})
|
assertUnknownEventFail(t, &ChannelActive{})
|
||||||
|
|
||||||
|
// Sending a Spend event should transition to CloseFin.
|
||||||
|
assertSpendEventCloseFin(t, &ChannelActive{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestRbfShutdownPendingTransitions tests the transitions of the RBF closer
|
// TestRbfShutdownPendingTransitions tests the transitions of the RBF closer
|
||||||
@@ -1033,8 +1085,106 @@ func TestRbfShutdownPendingTransitions(t *testing.T) {
|
|||||||
closeHarness.assertStateTransitions(&ChannelFlushing{})
|
closeHarness.assertStateTransitions(&ChannelFlushing{})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// If we an early offer from the remote party, then we should stash
|
||||||
|
// that, transition to the channel flushing state. Once there, another
|
||||||
|
// self transition should emit the stashed offer.
|
||||||
|
t.Run("early_remote_offer_shutdown_complete", func(t *testing.T) {
|
||||||
|
firstState := *startingState
|
||||||
|
firstState.IdealFeeRate = fn.Some(
|
||||||
|
chainfee.FeePerKwFloor.FeePerVByte(),
|
||||||
|
)
|
||||||
|
firstState.ShutdownScripts = ShutdownScripts{
|
||||||
|
LocalDeliveryScript: localAddr,
|
||||||
|
RemoteDeliveryScript: remoteAddr,
|
||||||
|
}
|
||||||
|
|
||||||
|
closeHarness := newCloser(t, &harnessCfg{
|
||||||
|
initialState: fn.Some[ProtocolState](
|
||||||
|
&firstState,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
defer closeHarness.stopAndAssert()
|
||||||
|
|
||||||
|
// In this case we're doing the shutdown dance for the first
|
||||||
|
// time, so we'll mark the channel as not being flushed.
|
||||||
|
closeHarness.expectFinalBalances(fn.None[ShutdownBalances]())
|
||||||
|
|
||||||
|
// Before we send the shutdown complete event, we'll send in an
|
||||||
|
// early offer from the remote party.
|
||||||
|
closeHarness.chanCloser.SendEvent(ctx, &OfferReceivedEvent{})
|
||||||
|
|
||||||
|
// This will cause a self transition back to ShutdownPending.
|
||||||
|
closeHarness.assertStateTransitions(&ShutdownPending{})
|
||||||
|
|
||||||
|
// Next, we'll send in a shutdown complete event.
|
||||||
|
closeHarness.chanCloser.SendEvent(ctx, &ShutdownComplete{})
|
||||||
|
|
||||||
|
// We should transition to the channel flushing state, then the
|
||||||
|
// self event to have this state cache he early offer should
|
||||||
|
// follow.
|
||||||
|
closeHarness.assertStateTransitions(
|
||||||
|
&ChannelFlushing{}, &ChannelFlushing{},
|
||||||
|
)
|
||||||
|
|
||||||
|
// If we get the current state, we should see that the offer is
|
||||||
|
// cached.
|
||||||
|
currentState := assertStateT[*ChannelFlushing](closeHarness)
|
||||||
|
require.NotNil(t, currentState.EarlyRemoteOffer)
|
||||||
|
})
|
||||||
|
|
||||||
|
// If we an early offer from the remote party, then we should stash
|
||||||
|
// that, transition to the channel flushing state. Once there, another
|
||||||
|
// self transition should emit the stashed offer.
|
||||||
|
t.Run("early_remote_offer_shutdown_received", func(t *testing.T) {
|
||||||
|
firstState := *startingState
|
||||||
|
firstState.IdealFeeRate = fn.Some(
|
||||||
|
chainfee.FeePerKwFloor.FeePerVByte(),
|
||||||
|
)
|
||||||
|
firstState.ShutdownScripts = ShutdownScripts{
|
||||||
|
LocalDeliveryScript: localAddr,
|
||||||
|
RemoteDeliveryScript: remoteAddr,
|
||||||
|
}
|
||||||
|
|
||||||
|
closeHarness := newCloser(t, &harnessCfg{
|
||||||
|
initialState: fn.Some[ProtocolState](
|
||||||
|
&firstState,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
defer closeHarness.stopAndAssert()
|
||||||
|
|
||||||
|
// In this case we're doing the shutdown dance for the first
|
||||||
|
// time, so we'll mark the channel as not being flushed.
|
||||||
|
closeHarness.expectFinalBalances(fn.None[ShutdownBalances]())
|
||||||
|
closeHarness.expectIncomingAddsDisabled()
|
||||||
|
|
||||||
|
// Before we send the shutdown complete event, we'll send in an
|
||||||
|
// early offer from the remote party.
|
||||||
|
closeHarness.chanCloser.SendEvent(ctx, &OfferReceivedEvent{})
|
||||||
|
|
||||||
|
// This will cause a self transition back to ShutdownPending.
|
||||||
|
closeHarness.assertStateTransitions(&ShutdownPending{})
|
||||||
|
|
||||||
|
// Next, we'll send in a shutdown complete event.
|
||||||
|
closeHarness.chanCloser.SendEvent(ctx, &ShutdownReceived{})
|
||||||
|
|
||||||
|
// We should transition to the channel flushing state, then the
|
||||||
|
// self event to have this state cache he early offer should
|
||||||
|
// follow.
|
||||||
|
closeHarness.assertStateTransitions(
|
||||||
|
&ChannelFlushing{}, &ChannelFlushing{},
|
||||||
|
)
|
||||||
|
|
||||||
|
// If we get the current state, we should see that the offer is
|
||||||
|
// cached.
|
||||||
|
currentState := assertStateT[*ChannelFlushing](closeHarness)
|
||||||
|
require.NotNil(t, currentState.EarlyRemoteOffer)
|
||||||
|
})
|
||||||
|
|
||||||
// Any other event should be ignored.
|
// Any other event should be ignored.
|
||||||
assertUnknownEventFail(t, startingState)
|
assertUnknownEventFail(t, startingState)
|
||||||
|
|
||||||
|
// Sending a Spend event should transition to CloseFin.
|
||||||
|
assertSpendEventCloseFin(t, startingState)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestRbfChannelFlushingTransitions tests the transitions of the RBF closer
|
// TestRbfChannelFlushingTransitions tests the transitions of the RBF closer
|
||||||
@@ -1142,7 +1292,7 @@ func TestRbfChannelFlushingTransitions(t *testing.T) {
|
|||||||
closeHarness.expectChanPendingClose()
|
closeHarness.expectChanPendingClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
// From where, we expect the state transition to go
|
// From here, we expect the state transition to go
|
||||||
// back to closing negotiated, for a ClosingComplete
|
// back to closing negotiated, for a ClosingComplete
|
||||||
// message to be sent and then for us to terminate at
|
// message to be sent and then for us to terminate at
|
||||||
// that state. This is 1/2 of the normal RBF signer
|
// that state. This is 1/2 of the normal RBF signer
|
||||||
@@ -1154,8 +1304,65 @@ func TestRbfChannelFlushingTransitions(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This tests that if we receive an `OfferReceivedEvent` while in the
|
||||||
|
// flushing state, then we'll cache that, and once we receive
|
||||||
|
// ChannelFlushed, we'll emit an internal `OfferReceivedEvent` in the
|
||||||
|
// negotiation state.
|
||||||
|
t.Run("early_offer", func(t *testing.T) {
|
||||||
|
firstState := *startingState
|
||||||
|
|
||||||
|
closeHarness := newCloser(t, &harnessCfg{
|
||||||
|
initialState: fn.Some[ProtocolState](
|
||||||
|
&firstState,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
defer closeHarness.stopAndAssert()
|
||||||
|
|
||||||
|
flushEvent := *flushTemplate
|
||||||
|
|
||||||
|
// Set up the fee estimate s.t the local party doesn't have
|
||||||
|
// balance to close.
|
||||||
|
closeHarness.expectFeeEstimate(absoluteFee, 1)
|
||||||
|
|
||||||
|
// First, we'll emit an `OfferReceivedEvent` to simulate an
|
||||||
|
// early offer (async network, they determine the channel is
|
||||||
|
// "flushed" before we do, and send their offer over).
|
||||||
|
remoteOffer := &OfferReceivedEvent{
|
||||||
|
SigMsg: lnwire.ClosingComplete{
|
||||||
|
FeeSatoshis: absoluteFee,
|
||||||
|
CloserScript: remoteAddr,
|
||||||
|
CloseeScript: localAddr,
|
||||||
|
ClosingSigs: lnwire.ClosingSigs{
|
||||||
|
CloserAndClosee: newSigTlv[tlv.TlvType3]( //nolint:ll
|
||||||
|
remoteWireSig,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
closeHarness.chanCloser.SendEvent(ctx, remoteOffer)
|
||||||
|
|
||||||
|
// We should do a self transition, and still be in the
|
||||||
|
// ChannelFlushing state.
|
||||||
|
closeHarness.assertStateTransitions(&ChannelFlushing{})
|
||||||
|
|
||||||
|
sequence := uint32(mempool.MaxRBFSequence)
|
||||||
|
|
||||||
|
// Now we'll send in the channel flushed event, and assert that
|
||||||
|
// this triggers a remote RBF iteration (we process their early
|
||||||
|
// offer and send our sig).
|
||||||
|
closeHarness.chanCloser.SendEvent(ctx, &flushEvent)
|
||||||
|
closeHarness.assertSingleRemoteRbfIteration(
|
||||||
|
remoteOffer, absoluteFee, absoluteFee, sequence, false,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
// Any other event should be ignored.
|
// Any other event should be ignored.
|
||||||
assertUnknownEventFail(t, startingState)
|
assertUnknownEventFail(t, startingState)
|
||||||
|
|
||||||
|
// Sending a Spend event should transition to CloseFin.
|
||||||
|
assertSpendEventCloseFin(t, startingState)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestRbfCloseClosingNegotiationLocal tests the local portion of the primary
|
// TestRbfCloseClosingNegotiationLocal tests the local portion of the primary
|
||||||
@@ -1184,9 +1391,10 @@ func TestRbfCloseClosingNegotiationLocal(t *testing.T) {
|
|||||||
startingState := &ClosingNegotiation{
|
startingState := &ClosingNegotiation{
|
||||||
PeerState: lntypes.Dual[AsymmetricPeerState]{
|
PeerState: lntypes.Dual[AsymmetricPeerState]{
|
||||||
Local: &LocalCloseStart{
|
Local: &LocalCloseStart{
|
||||||
CloseChannelTerms: *closeTerms,
|
CloseChannelTerms: closeTerms,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
CloseChannelTerms: closeTerms,
|
||||||
}
|
}
|
||||||
|
|
||||||
sendOfferEvent := &SendOfferEvent{
|
sendOfferEvent := &SendOfferEvent{
|
||||||
@@ -1195,6 +1403,8 @@ func TestRbfCloseClosingNegotiationLocal(t *testing.T) {
|
|||||||
|
|
||||||
balanceAfterClose := localBalance.ToSatoshis() - absoluteFee
|
balanceAfterClose := localBalance.ToSatoshis() - absoluteFee
|
||||||
|
|
||||||
|
// TODO(roasbeef): add test case for error state validation, then resume
|
||||||
|
|
||||||
// In this state, we'll simulate deciding that we need to send a new
|
// In this state, we'll simulate deciding that we need to send a new
|
||||||
// offer to the remote party.
|
// offer to the remote party.
|
||||||
t.Run("send_offer_iteration_no_dust", func(t *testing.T) {
|
t.Run("send_offer_iteration_no_dust", func(t *testing.T) {
|
||||||
@@ -1231,6 +1441,8 @@ func TestRbfCloseClosingNegotiationLocal(t *testing.T) {
|
|||||||
// we'll specify 2 signature fields.
|
// we'll specify 2 signature fields.
|
||||||
localSigEvent := &LocalSigReceived{
|
localSigEvent := &LocalSigReceived{
|
||||||
SigMsg: lnwire.ClosingSig{
|
SigMsg: lnwire.ClosingSig{
|
||||||
|
CloserScript: localAddr,
|
||||||
|
CloseeScript: remoteAddr,
|
||||||
ClosingSigs: lnwire.ClosingSigs{
|
ClosingSigs: lnwire.ClosingSigs{
|
||||||
CloserNoClosee: newSigTlv[tlv.TlvType1](
|
CloserNoClosee: newSigTlv[tlv.TlvType1](
|
||||||
remoteWireSig,
|
remoteWireSig,
|
||||||
@@ -1261,9 +1473,10 @@ func TestRbfCloseClosingNegotiationLocal(t *testing.T) {
|
|||||||
firstState := &ClosingNegotiation{
|
firstState := &ClosingNegotiation{
|
||||||
PeerState: lntypes.Dual[AsymmetricPeerState]{
|
PeerState: lntypes.Dual[AsymmetricPeerState]{
|
||||||
Local: &LocalCloseStart{
|
Local: &LocalCloseStart{
|
||||||
CloseChannelTerms: newCloseTerms,
|
CloseChannelTerms: &newCloseTerms,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
CloseChannelTerms: &newCloseTerms,
|
||||||
}
|
}
|
||||||
|
|
||||||
closeHarness := newCloser(t, &harnessCfg{
|
closeHarness := newCloser(t, &harnessCfg{
|
||||||
@@ -1286,9 +1499,10 @@ func TestRbfCloseClosingNegotiationLocal(t *testing.T) {
|
|||||||
firstState := &ClosingNegotiation{
|
firstState := &ClosingNegotiation{
|
||||||
PeerState: lntypes.Dual[AsymmetricPeerState]{
|
PeerState: lntypes.Dual[AsymmetricPeerState]{
|
||||||
Local: &LocalCloseStart{
|
Local: &LocalCloseStart{
|
||||||
CloseChannelTerms: *closeTerms,
|
CloseChannelTerms: closeTerms,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
CloseChannelTerms: closeTerms,
|
||||||
}
|
}
|
||||||
|
|
||||||
closeHarness := newCloser(t, &harnessCfg{
|
closeHarness := newCloser(t, &harnessCfg{
|
||||||
@@ -1306,15 +1520,55 @@ func TestRbfCloseClosingNegotiationLocal(t *testing.T) {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// In this test, we'll assert that we're able to restart the RBF loop
|
||||||
|
// to trigger additional signature iterations.
|
||||||
|
t.Run("send_offer_rbf_wrong_local_script", func(t *testing.T) {
|
||||||
|
firstState := &ClosingNegotiation{
|
||||||
|
PeerState: lntypes.Dual[AsymmetricPeerState]{
|
||||||
|
Local: &LocalCloseStart{
|
||||||
|
CloseChannelTerms: closeTerms,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CloseChannelTerms: closeTerms,
|
||||||
|
}
|
||||||
|
|
||||||
|
closeHarness := newCloser(t, &harnessCfg{
|
||||||
|
initialState: fn.Some[ProtocolState](firstState),
|
||||||
|
localUpfrontAddr: fn.Some(localAddr),
|
||||||
|
})
|
||||||
|
defer closeHarness.stopAndAssert()
|
||||||
|
|
||||||
|
// The remote party will send a ClosingSig message, but with the
|
||||||
|
// wrong local script. We should expect an error.
|
||||||
|
closeHarness.expectFailure(ErrWrongLocalScript)
|
||||||
|
|
||||||
|
// We'll send this message in directly, as we shouldn't get any
|
||||||
|
// further in the process.
|
||||||
|
// assuming we start in this negotiation state.
|
||||||
|
localSigEvent := &LocalSigReceived{
|
||||||
|
SigMsg: lnwire.ClosingSig{
|
||||||
|
CloserScript: remoteAddr,
|
||||||
|
CloseeScript: remoteAddr,
|
||||||
|
ClosingSigs: lnwire.ClosingSigs{
|
||||||
|
CloserAndClosee: newSigTlv[tlv.TlvType3]( //nolint:ll
|
||||||
|
remoteWireSig,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
closeHarness.chanCloser.SendEvent(ctx, localSigEvent)
|
||||||
|
})
|
||||||
|
|
||||||
// In this test, we'll assert that we're able to restart the RBF loop
|
// In this test, we'll assert that we're able to restart the RBF loop
|
||||||
// to trigger additional signature iterations.
|
// to trigger additional signature iterations.
|
||||||
t.Run("send_offer_rbf_iteration_loop", func(t *testing.T) {
|
t.Run("send_offer_rbf_iteration_loop", func(t *testing.T) {
|
||||||
firstState := &ClosingNegotiation{
|
firstState := &ClosingNegotiation{
|
||||||
PeerState: lntypes.Dual[AsymmetricPeerState]{
|
PeerState: lntypes.Dual[AsymmetricPeerState]{
|
||||||
Local: &LocalCloseStart{
|
Local: &LocalCloseStart{
|
||||||
CloseChannelTerms: *closeTerms,
|
CloseChannelTerms: closeTerms,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
CloseChannelTerms: closeTerms,
|
||||||
}
|
}
|
||||||
|
|
||||||
closeHarness := newCloser(t, &harnessCfg{
|
closeHarness := newCloser(t, &harnessCfg{
|
||||||
@@ -1330,44 +1584,18 @@ func TestRbfCloseClosingNegotiationLocal(t *testing.T) {
|
|||||||
noDustExpect,
|
noDustExpect,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Next, we'll send in a new SendShutdown event which simulates
|
// Next, we'll send in a new SendOfferEvent event which
|
||||||
// the user requesting a RBF fee bump. We'll use 10x the fee we
|
// simulates the user requesting a RBF fee bump. We'll use 10x
|
||||||
// used in the last iteration.
|
// the fee we used in the last iteration.
|
||||||
rbfFeeBump := chainfee.FeePerKwFloor.FeePerVByte() * 10
|
rbfFeeBump := chainfee.FeePerKwFloor.FeePerVByte() * 10
|
||||||
sendShutdown := &SendShutdown{
|
localOffer := &SendOfferEvent{
|
||||||
IdealFeeRate: rbfFeeBump,
|
TargetFeeRate: rbfFeeBump,
|
||||||
}
|
}
|
||||||
|
|
||||||
// We should send shutdown as normal, but skip some other
|
// Now we expect that another full RBF iteration takes place (we
|
||||||
// checks as we know the close is in progress.
|
// initiate a new local sig).
|
||||||
closeHarness.expectShutdownEvents(shutdownExpect{
|
|
||||||
allowSend: true,
|
|
||||||
finalBalances: fn.Some(closeTerms.ShutdownBalances),
|
|
||||||
recvShutdown: true,
|
|
||||||
})
|
|
||||||
closeHarness.expectMsgSent(
|
|
||||||
singleMsgMatcher[*lnwire.Shutdown](nil),
|
|
||||||
)
|
|
||||||
|
|
||||||
closeHarness.chanCloser.SendEvent(ctx, sendShutdown)
|
|
||||||
|
|
||||||
// We should first transition to the Channel Active state
|
|
||||||
// momentarily, before transitioning to the shutdown pending
|
|
||||||
// state.
|
|
||||||
closeHarness.assertStateTransitions(
|
|
||||||
&ChannelActive{}, &ShutdownPending{},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Next, we'll send in the shutdown received event, which
|
|
||||||
// should transition us to the channel flushing state.
|
|
||||||
shutdownEvent := &ShutdownReceived{
|
|
||||||
ShutdownScript: remoteAddr,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now we expect that aanother full RBF iteration takes place
|
|
||||||
// (we initiatea a new local sig).
|
|
||||||
closeHarness.assertSingleRbfIteration(
|
closeHarness.assertSingleRbfIteration(
|
||||||
shutdownEvent, balanceAfterClose, absoluteFee,
|
localOffer, balanceAfterClose, absoluteFee,
|
||||||
noDustExpect,
|
noDustExpect,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1376,6 +1604,59 @@ func TestRbfCloseClosingNegotiationLocal(t *testing.T) {
|
|||||||
&ClosingNegotiation{},
|
&ClosingNegotiation{},
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Make sure that we'll go to the error state if we try to try a close
|
||||||
|
// that we can't pay for.
|
||||||
|
t.Run("send_offer_cannot_pay_for_fees", func(t *testing.T) {
|
||||||
|
firstState := &ClosingNegotiation{
|
||||||
|
PeerState: lntypes.Dual[AsymmetricPeerState]{
|
||||||
|
Local: &LocalCloseStart{
|
||||||
|
CloseChannelTerms: closeTerms,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CloseChannelTerms: closeTerms,
|
||||||
|
}
|
||||||
|
|
||||||
|
closeHarness := newCloser(t, &harnessCfg{
|
||||||
|
initialState: fn.Some[ProtocolState](firstState),
|
||||||
|
localUpfrontAddr: fn.Some(localAddr),
|
||||||
|
})
|
||||||
|
defer closeHarness.stopAndAssert()
|
||||||
|
|
||||||
|
// We'll prep to return an absolute fee that's much higher than
|
||||||
|
// the amount we have in the channel.
|
||||||
|
closeHarness.expectFeeEstimate(btcutil.SatoshiPerBitcoin, 1)
|
||||||
|
|
||||||
|
rbfFeeBump := chainfee.FeePerKwFloor.FeePerVByte()
|
||||||
|
localOffer := &SendOfferEvent{
|
||||||
|
TargetFeeRate: rbfFeeBump,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next, we'll send in this event, which should fail as we can't
|
||||||
|
// actually pay for fees.
|
||||||
|
closeHarness.chanCloser.SendEvent(ctx, localOffer)
|
||||||
|
|
||||||
|
// We should transition to the CloseErr (within
|
||||||
|
// ClosingNegotiation) state.
|
||||||
|
closeHarness.assertStateTransitions(&ClosingNegotiation{})
|
||||||
|
|
||||||
|
// If we get the state, we should see the expected ErrState.
|
||||||
|
currentState := assertStateT[*ClosingNegotiation](closeHarness)
|
||||||
|
|
||||||
|
closeErrState, ok := currentState.PeerState.GetForParty(
|
||||||
|
lntypes.Local,
|
||||||
|
).(*CloseErr)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.IsType(
|
||||||
|
t, &ErrStateCantPayForFee{}, closeErrState.ErrState,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Any other event should be ignored.
|
||||||
|
assertUnknownEventFail(t, startingState)
|
||||||
|
|
||||||
|
// Sending a Spend event should transition to CloseFin.
|
||||||
|
assertSpendEventCloseFin(t, startingState)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestRbfCloseClosingNegotiationRemote tests that state machine is able to
|
// TestRbfCloseClosingNegotiationRemote tests that state machine is able to
|
||||||
@@ -1383,6 +1664,7 @@ func TestRbfCloseClosingNegotiationLocal(t *testing.T) {
|
|||||||
// party.
|
// party.
|
||||||
func TestRbfCloseClosingNegotiationRemote(t *testing.T) {
|
func TestRbfCloseClosingNegotiationRemote(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
localBalance := lnwire.NewMSatFromSatoshis(40_000)
|
localBalance := lnwire.NewMSatFromSatoshis(40_000)
|
||||||
@@ -1403,16 +1685,16 @@ func TestRbfCloseClosingNegotiationRemote(t *testing.T) {
|
|||||||
startingState := &ClosingNegotiation{
|
startingState := &ClosingNegotiation{
|
||||||
PeerState: lntypes.Dual[AsymmetricPeerState]{
|
PeerState: lntypes.Dual[AsymmetricPeerState]{
|
||||||
Local: &LocalCloseStart{
|
Local: &LocalCloseStart{
|
||||||
CloseChannelTerms: *closeTerms,
|
CloseChannelTerms: closeTerms,
|
||||||
},
|
},
|
||||||
Remote: &RemoteCloseStart{
|
Remote: &RemoteCloseStart{
|
||||||
CloseChannelTerms: *closeTerms,
|
CloseChannelTerms: closeTerms,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
CloseChannelTerms: closeTerms,
|
||||||
}
|
}
|
||||||
|
|
||||||
balanceAfterClose := remoteBalance.ToSatoshis() - absoluteFee
|
balanceAfterClose := remoteBalance.ToSatoshis() - absoluteFee
|
||||||
|
|
||||||
sequence := uint32(mempool.MaxRBFSequence)
|
sequence := uint32(mempool.MaxRBFSequence)
|
||||||
|
|
||||||
// This case tests that if we receive a signature from the remote
|
// This case tests that if we receive a signature from the remote
|
||||||
@@ -1430,6 +1712,8 @@ func TestRbfCloseClosingNegotiationRemote(t *testing.T) {
|
|||||||
// be higher than the remote party's balance.
|
// be higher than the remote party's balance.
|
||||||
feeOffer := &OfferReceivedEvent{
|
feeOffer := &OfferReceivedEvent{
|
||||||
SigMsg: lnwire.ClosingComplete{
|
SigMsg: lnwire.ClosingComplete{
|
||||||
|
CloserScript: remoteAddr,
|
||||||
|
CloseeScript: localAddr,
|
||||||
FeeSatoshis: absoluteFee * 10,
|
FeeSatoshis: absoluteFee * 10,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1449,12 +1733,13 @@ func TestRbfCloseClosingNegotiationRemote(t *testing.T) {
|
|||||||
firstState := &ClosingNegotiation{
|
firstState := &ClosingNegotiation{
|
||||||
PeerState: lntypes.Dual[AsymmetricPeerState]{
|
PeerState: lntypes.Dual[AsymmetricPeerState]{
|
||||||
Local: &LocalCloseStart{
|
Local: &LocalCloseStart{
|
||||||
CloseChannelTerms: closingTerms,
|
CloseChannelTerms: &closingTerms,
|
||||||
},
|
},
|
||||||
Remote: &RemoteCloseStart{
|
Remote: &RemoteCloseStart{
|
||||||
CloseChannelTerms: closingTerms,
|
CloseChannelTerms: &closingTerms,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
CloseChannelTerms: &closingTerms,
|
||||||
}
|
}
|
||||||
|
|
||||||
closeHarness := newCloser(t, &harnessCfg{
|
closeHarness := newCloser(t, &harnessCfg{
|
||||||
@@ -1470,6 +1755,8 @@ func TestRbfCloseClosingNegotiationRemote(t *testing.T) {
|
|||||||
feeOffer := &OfferReceivedEvent{
|
feeOffer := &OfferReceivedEvent{
|
||||||
SigMsg: lnwire.ClosingComplete{
|
SigMsg: lnwire.ClosingComplete{
|
||||||
FeeSatoshis: absoluteFee,
|
FeeSatoshis: absoluteFee,
|
||||||
|
CloserScript: remoteAddr,
|
||||||
|
CloseeScript: localAddr,
|
||||||
ClosingSigs: lnwire.ClosingSigs{
|
ClosingSigs: lnwire.ClosingSigs{
|
||||||
CloserAndClosee: newSigTlv[tlv.TlvType3]( //nolint:ll
|
CloserAndClosee: newSigTlv[tlv.TlvType3]( //nolint:ll
|
||||||
remoteWireSig,
|
remoteWireSig,
|
||||||
@@ -1499,6 +1786,8 @@ func TestRbfCloseClosingNegotiationRemote(t *testing.T) {
|
|||||||
feeOffer := &OfferReceivedEvent{
|
feeOffer := &OfferReceivedEvent{
|
||||||
SigMsg: lnwire.ClosingComplete{
|
SigMsg: lnwire.ClosingComplete{
|
||||||
FeeSatoshis: absoluteFee,
|
FeeSatoshis: absoluteFee,
|
||||||
|
CloserScript: remoteAddr,
|
||||||
|
CloseeScript: localAddr,
|
||||||
ClosingSigs: lnwire.ClosingSigs{
|
ClosingSigs: lnwire.ClosingSigs{
|
||||||
CloserNoClosee: newSigTlv[tlv.TlvType1]( //nolint:ll
|
CloserNoClosee: newSigTlv[tlv.TlvType1]( //nolint:ll
|
||||||
remoteWireSig,
|
remoteWireSig,
|
||||||
@@ -1516,8 +1805,8 @@ func TestRbfCloseClosingNegotiationRemote(t *testing.T) {
|
|||||||
// loops to enable the remote party to sign.new versions of the co-op
|
// loops to enable the remote party to sign.new versions of the co-op
|
||||||
// close transaction.
|
// close transaction.
|
||||||
t.Run("recv_offer_rbf_loop_iterations", func(t *testing.T) {
|
t.Run("recv_offer_rbf_loop_iterations", func(t *testing.T) {
|
||||||
// We'll modify our s.t we're unable to pay for fees, but
|
// We'll modify our balance s.t we're unable to pay for fees,
|
||||||
// aren't yet dust.
|
// but aren't yet dust.
|
||||||
closingTerms := *closeTerms
|
closingTerms := *closeTerms
|
||||||
closingTerms.ShutdownBalances.LocalBalance = lnwire.NewMSatFromSatoshis( //nolint:ll
|
closingTerms.ShutdownBalances.LocalBalance = lnwire.NewMSatFromSatoshis( //nolint:ll
|
||||||
9000,
|
9000,
|
||||||
@@ -1526,12 +1815,13 @@ func TestRbfCloseClosingNegotiationRemote(t *testing.T) {
|
|||||||
firstState := &ClosingNegotiation{
|
firstState := &ClosingNegotiation{
|
||||||
PeerState: lntypes.Dual[AsymmetricPeerState]{
|
PeerState: lntypes.Dual[AsymmetricPeerState]{
|
||||||
Local: &LocalCloseStart{
|
Local: &LocalCloseStart{
|
||||||
CloseChannelTerms: closingTerms,
|
CloseChannelTerms: &closingTerms,
|
||||||
},
|
},
|
||||||
Remote: &RemoteCloseStart{
|
Remote: &RemoteCloseStart{
|
||||||
CloseChannelTerms: closingTerms,
|
CloseChannelTerms: &closingTerms,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
CloseChannelTerms: &closingTerms,
|
||||||
}
|
}
|
||||||
|
|
||||||
closeHarness := newCloser(t, &harnessCfg{
|
closeHarness := newCloser(t, &harnessCfg{
|
||||||
@@ -1542,6 +1832,8 @@ func TestRbfCloseClosingNegotiationRemote(t *testing.T) {
|
|||||||
|
|
||||||
feeOffer := &OfferReceivedEvent{
|
feeOffer := &OfferReceivedEvent{
|
||||||
SigMsg: lnwire.ClosingComplete{
|
SigMsg: lnwire.ClosingComplete{
|
||||||
|
CloserScript: remoteAddr,
|
||||||
|
CloseeScript: localAddr,
|
||||||
FeeSatoshis: absoluteFee,
|
FeeSatoshis: absoluteFee,
|
||||||
LockTime: 1,
|
LockTime: 1,
|
||||||
ClosingSigs: lnwire.ClosingSigs{
|
ClosingSigs: lnwire.ClosingSigs{
|
||||||
@@ -1557,43 +1849,214 @@ func TestRbfCloseClosingNegotiationRemote(t *testing.T) {
|
|||||||
// sig.
|
// sig.
|
||||||
closeHarness.assertSingleRemoteRbfIteration(
|
closeHarness.assertSingleRemoteRbfIteration(
|
||||||
feeOffer, balanceAfterClose, absoluteFee, sequence,
|
feeOffer, balanceAfterClose, absoluteFee, sequence,
|
||||||
false,
|
false, true,
|
||||||
)
|
)
|
||||||
|
|
||||||
// At this point, we've completed a single RBF iteration, and
|
// Next, we'll receive an offer from the remote party, and drive
|
||||||
// want to test further iterations, so we'll use a shutdown
|
// another RBF iteration. This time, we'll increase the absolute
|
||||||
// even tot kick it all off.
|
// fee by 1k sats.
|
||||||
//
|
|
||||||
// Before we send the shutdown messages below, we'll mark the
|
|
||||||
// balances as so we fast track to the negotiation state.
|
|
||||||
closeHarness.expectShutdownEvents(shutdownExpect{
|
|
||||||
allowSend: true,
|
|
||||||
finalBalances: fn.Some(closingTerms.ShutdownBalances),
|
|
||||||
recvShutdown: true,
|
|
||||||
})
|
|
||||||
closeHarness.expectMsgSent(
|
|
||||||
singleMsgMatcher[*lnwire.Shutdown](nil),
|
|
||||||
)
|
|
||||||
|
|
||||||
// We'll now simulate the start of the RBF loop, by receiving a
|
|
||||||
// new Shutdown message from the remote party. This signals
|
|
||||||
// that they want to obtain a new commit sig.
|
|
||||||
closeHarness.chanCloser.SendEvent(
|
|
||||||
ctx, &ShutdownReceived{ShutdownScript: remoteAddr},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Next, we'll receive an offer from the remote party, and
|
|
||||||
// drive another RBF iteration. This time, we'll increase the
|
|
||||||
// absolute fee by 1k sats.
|
|
||||||
feeOffer.SigMsg.FeeSatoshis += 1000
|
feeOffer.SigMsg.FeeSatoshis += 1000
|
||||||
absoluteFee = feeOffer.SigMsg.FeeSatoshis
|
absoluteFee = feeOffer.SigMsg.FeeSatoshis
|
||||||
closeHarness.assertSingleRemoteRbfIteration(
|
closeHarness.assertSingleRemoteRbfIteration(
|
||||||
feeOffer, balanceAfterClose, absoluteFee, sequence,
|
feeOffer, balanceAfterClose, absoluteFee, sequence,
|
||||||
true,
|
true, true,
|
||||||
)
|
)
|
||||||
|
|
||||||
closeHarness.assertNoStateTransitions()
|
closeHarness.assertNoStateTransitions()
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO(roasbeef): cross sig case? tested isolation, so wolog?
|
// This tests that if we get an offer that has the wrong local script,
|
||||||
|
// then we'll emit a hard error.
|
||||||
|
t.Run("recv_offer_wrong_local_script", func(t *testing.T) {
|
||||||
|
closeHarness := newCloser(t, &harnessCfg{
|
||||||
|
initialState: fn.Some[ProtocolState](startingState),
|
||||||
|
})
|
||||||
|
defer closeHarness.stopAndAssert()
|
||||||
|
|
||||||
|
// The remote party will send a ClosingComplete message, but
|
||||||
|
// with the wrong local script. We should expect an error.
|
||||||
|
closeHarness.expectFailure(ErrWrongLocalScript)
|
||||||
|
|
||||||
|
// We'll send our remote addr as the Closee script, which should
|
||||||
|
// trigger an error.
|
||||||
|
feeOffer := &OfferReceivedEvent{
|
||||||
|
SigMsg: lnwire.ClosingComplete{
|
||||||
|
FeeSatoshis: absoluteFee,
|
||||||
|
CloserScript: remoteAddr,
|
||||||
|
CloseeScript: remoteAddr,
|
||||||
|
ClosingSigs: lnwire.ClosingSigs{
|
||||||
|
CloserNoClosee: newSigTlv[tlv.TlvType1]( //nolint:ll
|
||||||
|
remoteWireSig,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
closeHarness.chanCloser.SendEvent(ctx, feeOffer)
|
||||||
|
|
||||||
|
// We shouldn't have transitioned to a new state.
|
||||||
|
closeHarness.assertNoStateTransitions()
|
||||||
|
})
|
||||||
|
|
||||||
|
// If we receive an offer from the remote party with a different remote
|
||||||
|
// script, then this ensures that we'll process that and use that create
|
||||||
|
// the next offer.
|
||||||
|
t.Run("recv_offer_remote_addr_change", func(t *testing.T) {
|
||||||
|
closingTerms := *closeTerms
|
||||||
|
|
||||||
|
firstState := &ClosingNegotiation{
|
||||||
|
PeerState: lntypes.Dual[AsymmetricPeerState]{
|
||||||
|
Local: &LocalCloseStart{
|
||||||
|
CloseChannelTerms: &closingTerms,
|
||||||
|
},
|
||||||
|
Remote: &RemoteCloseStart{
|
||||||
|
CloseChannelTerms: &closingTerms,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CloseChannelTerms: &closingTerms,
|
||||||
|
}
|
||||||
|
|
||||||
|
closeHarness := newCloser(t, &harnessCfg{
|
||||||
|
initialState: fn.Some[ProtocolState](firstState),
|
||||||
|
localUpfrontAddr: fn.Some(localAddr),
|
||||||
|
})
|
||||||
|
defer closeHarness.stopAndAssert()
|
||||||
|
|
||||||
|
// This time, the close request sent by the remote party will
|
||||||
|
// modify their normal remote address. This should cause us to
|
||||||
|
// recognize this, and counter sign the proper co-op close
|
||||||
|
// transaction.
|
||||||
|
newRemoteAddr := lnwire.DeliveryAddress(append(
|
||||||
|
[]byte{txscript.OP_1, txscript.OP_DATA_32},
|
||||||
|
bytes.Repeat([]byte{0x03}, 32)...,
|
||||||
|
))
|
||||||
|
feeOffer := &OfferReceivedEvent{
|
||||||
|
SigMsg: lnwire.ClosingComplete{
|
||||||
|
CloserScript: newRemoteAddr,
|
||||||
|
CloseeScript: localAddr,
|
||||||
|
FeeSatoshis: absoluteFee,
|
||||||
|
LockTime: 1,
|
||||||
|
ClosingSigs: lnwire.ClosingSigs{
|
||||||
|
CloserAndClosee: newSigTlv[tlv.TlvType3]( //nolint:ll
|
||||||
|
remoteWireSig,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// As we're already in the negotiation phase, we'll now trigger
|
||||||
|
// a new iteration by having the remote party send a new offer
|
||||||
|
// sig.
|
||||||
|
closeHarness.assertSingleRemoteRbfIteration(
|
||||||
|
feeOffer, balanceAfterClose, absoluteFee, sequence,
|
||||||
|
false, true,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Any other event should be ignored.
|
||||||
|
assertUnknownEventFail(t, startingState)
|
||||||
|
|
||||||
|
// Sending a Spend event should transition to CloseFin.
|
||||||
|
assertSpendEventCloseFin(t, startingState)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRbfCloseErr tests that the state machine is able to properly restart
|
||||||
|
// the state machine if we encounter an error.
|
||||||
|
func TestRbfCloseErr(t *testing.T) {
|
||||||
|
localBalance := lnwire.NewMSatFromSatoshis(40_000)
|
||||||
|
remoteBalance := lnwire.NewMSatFromSatoshis(50_000)
|
||||||
|
|
||||||
|
closeTerms := &CloseChannelTerms{
|
||||||
|
ShutdownBalances: ShutdownBalances{
|
||||||
|
LocalBalance: localBalance,
|
||||||
|
RemoteBalance: remoteBalance,
|
||||||
|
},
|
||||||
|
ShutdownScripts: ShutdownScripts{
|
||||||
|
LocalDeliveryScript: localAddr,
|
||||||
|
RemoteDeliveryScript: remoteAddr,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
startingState := &ClosingNegotiation{
|
||||||
|
PeerState: lntypes.Dual[AsymmetricPeerState]{
|
||||||
|
Local: &CloseErr{
|
||||||
|
CloseChannelTerms: closeTerms,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CloseChannelTerms: closeTerms,
|
||||||
|
}
|
||||||
|
|
||||||
|
absoluteFee := btcutil.Amount(10_100)
|
||||||
|
balanceAfterClose := localBalance.ToSatoshis() - absoluteFee
|
||||||
|
|
||||||
|
// From the error state, we should be able to kick off a new iteration
|
||||||
|
// for a local fee bump.
|
||||||
|
t.Run("send_offer_restart", func(t *testing.T) {
|
||||||
|
closeHarness := newCloser(t, &harnessCfg{
|
||||||
|
initialState: fn.Some[ProtocolState](startingState),
|
||||||
|
})
|
||||||
|
defer closeHarness.stopAndAssert()
|
||||||
|
|
||||||
|
rbfFeeBump := chainfee.FeePerKwFloor.FeePerVByte()
|
||||||
|
localOffer := &SendOfferEvent{
|
||||||
|
TargetFeeRate: rbfFeeBump,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we expect that another full RBF iteration takes place (we
|
||||||
|
// initiate a new local sig).
|
||||||
|
closeHarness.assertSingleRbfIteration(
|
||||||
|
localOffer, balanceAfterClose, absoluteFee,
|
||||||
|
noDustExpect,
|
||||||
|
)
|
||||||
|
|
||||||
|
// We should terminate in the negotiation state.
|
||||||
|
closeHarness.assertStateTransitions(
|
||||||
|
&ClosingNegotiation{},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// From the error state, we should be able to handle the remote party
|
||||||
|
// kicking off a new iteration for a fee bump.
|
||||||
|
t.Run("recv_offer_restart", func(t *testing.T) {
|
||||||
|
startingState := &ClosingNegotiation{
|
||||||
|
PeerState: lntypes.Dual[AsymmetricPeerState]{
|
||||||
|
Remote: &CloseErr{
|
||||||
|
CloseChannelTerms: closeTerms,
|
||||||
|
Party: lntypes.Remote,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CloseChannelTerms: closeTerms,
|
||||||
|
}
|
||||||
|
|
||||||
|
closeHarness := newCloser(t, &harnessCfg{
|
||||||
|
initialState: fn.Some[ProtocolState](startingState),
|
||||||
|
localUpfrontAddr: fn.Some(localAddr),
|
||||||
|
})
|
||||||
|
defer closeHarness.stopAndAssert()
|
||||||
|
|
||||||
|
feeOffer := &OfferReceivedEvent{
|
||||||
|
SigMsg: lnwire.ClosingComplete{
|
||||||
|
CloserScript: remoteAddr,
|
||||||
|
CloseeScript: localAddr,
|
||||||
|
FeeSatoshis: absoluteFee,
|
||||||
|
LockTime: 1,
|
||||||
|
ClosingSigs: lnwire.ClosingSigs{
|
||||||
|
CloserAndClosee: newSigTlv[tlv.TlvType3]( //nolint:ll
|
||||||
|
remoteWireSig,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
sequence := uint32(mempool.MaxRBFSequence)
|
||||||
|
|
||||||
|
// As we're already in the negotiation phase, we'll now trigger
|
||||||
|
// a new iteration by having the remote party send a new offer
|
||||||
|
// sig.
|
||||||
|
closeHarness.assertSingleRemoteRbfIteration(
|
||||||
|
feeOffer, balanceAfterClose, absoluteFee, sequence,
|
||||||
|
false, true,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Sending a Spend event should transition to CloseFin.
|
||||||
|
assertSpendEventCloseFin(t, startingState)
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
package chancloser
|
package chancloser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/btcec/v2"
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
"github.com/btcsuite/btcd/chaincfg"
|
"github.com/btcsuite/btcd/chaincfg"
|
||||||
"github.com/btcsuite/btcd/mempool"
|
"github.com/btcsuite/btcd/mempool"
|
||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
@@ -14,11 +16,18 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/lntypes"
|
"github.com/lightningnetwork/lnd/lntypes"
|
||||||
"github.com/lightningnetwork/lnd/lnutils"
|
"github.com/lightningnetwork/lnd/lnutils"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet"
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
"github.com/lightningnetwork/lnd/protofsm"
|
"github.com/lightningnetwork/lnd/protofsm"
|
||||||
"github.com/lightningnetwork/lnd/tlv"
|
"github.com/lightningnetwork/lnd/tlv"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrInvalidStateTransition is returned if the remote party tries to
|
||||||
|
// close, but the thaw height hasn't been matched yet.
|
||||||
|
ErrThawHeightNotReached = fmt.Errorf("thaw height not reached")
|
||||||
|
)
|
||||||
|
|
||||||
// sendShutdownEvents is a helper function that returns a set of daemon events
|
// sendShutdownEvents is a helper function that returns a set of daemon events
|
||||||
// we need to emit when we decide that we should send a shutdown message. We'll
|
// we need to emit when we decide that we should send a shutdown message. We'll
|
||||||
// also mark the channel as borked as well, as at this point, we no longer want
|
// also mark the channel as borked as well, as at this point, we no longer want
|
||||||
@@ -104,11 +113,11 @@ func validateShutdown(chanThawHeight fn.Option[uint32],
|
|||||||
// reject the shutdown message as we can't yet co-op close the
|
// reject the shutdown message as we can't yet co-op close the
|
||||||
// channel.
|
// channel.
|
||||||
if msg.BlockHeight < thawHeight {
|
if msg.BlockHeight < thawHeight {
|
||||||
return fmt.Errorf("initiator attempting to "+
|
return fmt.Errorf("%w: initiator attempting to "+
|
||||||
"co-op close frozen ChannelPoint(%v) "+
|
"co-op close frozen ChannelPoint(%v) "+
|
||||||
"(current_height=%v, thaw_height=%v)",
|
"(current_height=%v, thaw_height=%v)",
|
||||||
chanPoint, msg.BlockHeight,
|
ErrThawHeightNotReached, chanPoint,
|
||||||
thawHeight)
|
msg.BlockHeight, thawHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -171,7 +180,7 @@ func (c *ChannelActive) ProcessEvent(event ProtocolEvent, env *Environment,
|
|||||||
}
|
}
|
||||||
|
|
||||||
chancloserLog.Infof("ChannelPoint(%v): sending shutdown msg, "+
|
chancloserLog.Infof("ChannelPoint(%v): sending shutdown msg, "+
|
||||||
"delivery_script=%v", env.ChanPoint, shutdownScript)
|
"delivery_script=%x", env.ChanPoint, shutdownScript)
|
||||||
|
|
||||||
// From here, we'll transition to the shutdown pending state. In
|
// From here, we'll transition to the shutdown pending state. In
|
||||||
// this state we await their shutdown message (self loop), then
|
// this state we await their shutdown message (self loop), then
|
||||||
@@ -193,7 +202,8 @@ func (c *ChannelActive) ProcessEvent(event ProtocolEvent, env *Environment,
|
|||||||
// also emit similar events like the above to send out shutdown, and
|
// also emit similar events like the above to send out shutdown, and
|
||||||
// also disable the channel.
|
// also disable the channel.
|
||||||
case *ShutdownReceived:
|
case *ShutdownReceived:
|
||||||
chancloserLog.Infof("ChannelPoint(%v): received shutdown msg")
|
chancloserLog.Infof("ChannelPoint(%v): received shutdown msg",
|
||||||
|
env.ChanPoint)
|
||||||
|
|
||||||
// Validate that they can send the message now, and also that
|
// Validate that they can send the message now, and also that
|
||||||
// they haven't violated their commitment to a prior upfront
|
// they haven't violated their commitment to a prior upfront
|
||||||
@@ -204,7 +214,7 @@ func (c *ChannelActive) ProcessEvent(event ProtocolEvent, env *Environment,
|
|||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
chancloserLog.Errorf("ChannelPoint(%v): rejecting "+
|
chancloserLog.Errorf("ChannelPoint(%v): rejecting "+
|
||||||
"shutdown attempt: %v", err)
|
"shutdown attempt: %v", env.ChanPoint, err)
|
||||||
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -287,6 +297,22 @@ func (s *ShutdownPending) ProcessEvent(event ProtocolEvent, env *Environment,
|
|||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
|
|
||||||
|
// The remote party sent an offer early. We'll go to the ChannelFlushing
|
||||||
|
// case, and then emit the offer as a internal event, which'll be
|
||||||
|
// handled as an early offer.
|
||||||
|
case *OfferReceivedEvent:
|
||||||
|
chancloserLog.Infof("ChannelPoint(%v): got an early offer "+
|
||||||
|
"in ShutdownPending, emitting as external event",
|
||||||
|
env.ChanPoint)
|
||||||
|
|
||||||
|
s.EarlyRemoteOffer = fn.Some(*msg)
|
||||||
|
|
||||||
|
// We'll perform a noop update so we can wait for the actual
|
||||||
|
// channel flushed event.
|
||||||
|
return &CloseStateTransition{
|
||||||
|
NextState: s,
|
||||||
|
}, nil
|
||||||
|
|
||||||
// When we receive a shutdown from the remote party, we'll validate the
|
// When we receive a shutdown from the remote party, we'll validate the
|
||||||
// shutdown message, then transition to the ChannelFlushing state.
|
// shutdown message, then transition to the ChannelFlushing state.
|
||||||
case *ShutdownReceived:
|
case *ShutdownReceived:
|
||||||
@@ -302,7 +328,7 @@ func (s *ShutdownPending) ProcessEvent(event ProtocolEvent, env *Environment,
|
|||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
chancloserLog.Errorf("ChannelPoint(%v): rejecting "+
|
chancloserLog.Errorf("ChannelPoint(%v): rejecting "+
|
||||||
"shutdown attempt: %v", err)
|
"shutdown attempt: %v", env.ChanPoint, err)
|
||||||
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -310,7 +336,7 @@ func (s *ShutdownPending) ProcessEvent(event ProtocolEvent, env *Environment,
|
|||||||
// If the channel is *already* flushed, and the close is
|
// If the channel is *already* flushed, and the close is
|
||||||
// go straight into negotiation, as this is the RBF loop.
|
// go straight into negotiation, as this is the RBF loop.
|
||||||
// already in progress, then we can skip the flushing state and
|
// already in progress, then we can skip the flushing state and
|
||||||
var eventsToEmit fn.Option[protofsm.EmittedEvent[ProtocolEvent]]
|
var eventsToEmit []ProtocolEvent
|
||||||
finalBalances := env.ChanObserver.FinalBalances().UnwrapOr(
|
finalBalances := env.ChanObserver.FinalBalances().UnwrapOr(
|
||||||
unknownBalance,
|
unknownBalance,
|
||||||
)
|
)
|
||||||
@@ -318,11 +344,7 @@ func (s *ShutdownPending) ProcessEvent(event ProtocolEvent, env *Environment,
|
|||||||
channelFlushed := ProtocolEvent(&ChannelFlushed{
|
channelFlushed := ProtocolEvent(&ChannelFlushed{
|
||||||
ShutdownBalances: finalBalances,
|
ShutdownBalances: finalBalances,
|
||||||
})
|
})
|
||||||
eventsToEmit = fn.Some(RbfEvent{
|
eventsToEmit = append(eventsToEmit, channelFlushed)
|
||||||
InternalEvent: []ProtocolEvent{
|
|
||||||
channelFlushed,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
chancloserLog.Infof("ChannelPoint(%v): disabling incoming adds",
|
chancloserLog.Infof("ChannelPoint(%v): disabling incoming adds",
|
||||||
@@ -338,6 +360,19 @@ func (s *ShutdownPending) ProcessEvent(event ProtocolEvent, env *Environment,
|
|||||||
chancloserLog.Infof("ChannelPoint(%v): waiting for channel to "+
|
chancloserLog.Infof("ChannelPoint(%v): waiting for channel to "+
|
||||||
"be flushed...", env.ChanPoint)
|
"be flushed...", env.ChanPoint)
|
||||||
|
|
||||||
|
// If we received a remote offer early from the remote party,
|
||||||
|
// then we'll add that to the set of internal events to emit.
|
||||||
|
s.EarlyRemoteOffer.WhenSome(func(offer OfferReceivedEvent) {
|
||||||
|
eventsToEmit = append(eventsToEmit, &offer)
|
||||||
|
})
|
||||||
|
|
||||||
|
var newEvents fn.Option[RbfEvent]
|
||||||
|
if len(eventsToEmit) > 0 {
|
||||||
|
newEvents = fn.Some(RbfEvent{
|
||||||
|
InternalEvent: eventsToEmit,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// We transition to the ChannelFlushing state, where we await
|
// We transition to the ChannelFlushing state, where we await
|
||||||
// the ChannelFlushed event.
|
// the ChannelFlushed event.
|
||||||
return &CloseStateTransition{
|
return &CloseStateTransition{
|
||||||
@@ -348,7 +383,7 @@ func (s *ShutdownPending) ProcessEvent(event ProtocolEvent, env *Environment,
|
|||||||
RemoteDeliveryScript: msg.ShutdownScript, //nolint:ll
|
RemoteDeliveryScript: msg.ShutdownScript, //nolint:ll
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
NewEvents: eventsToEmit,
|
NewEvents: newEvents,
|
||||||
}, nil
|
}, nil
|
||||||
|
|
||||||
// If we get this message, then this means that we were finally able to
|
// If we get this message, then this means that we were finally able to
|
||||||
@@ -361,7 +396,7 @@ func (s *ShutdownPending) ProcessEvent(event ProtocolEvent, env *Environment,
|
|||||||
// If the channel is *already* flushed, and the close is
|
// If the channel is *already* flushed, and the close is
|
||||||
// already in progress, then we can skip the flushing state and
|
// already in progress, then we can skip the flushing state and
|
||||||
// go straight into negotiation, as this is the RBF loop.
|
// go straight into negotiation, as this is the RBF loop.
|
||||||
var eventsToEmit fn.Option[protofsm.EmittedEvent[ProtocolEvent]]
|
var eventsToEmit []ProtocolEvent
|
||||||
finalBalances := env.ChanObserver.FinalBalances().UnwrapOr(
|
finalBalances := env.ChanObserver.FinalBalances().UnwrapOr(
|
||||||
unknownBalance,
|
unknownBalance,
|
||||||
)
|
)
|
||||||
@@ -369,10 +404,19 @@ func (s *ShutdownPending) ProcessEvent(event ProtocolEvent, env *Environment,
|
|||||||
channelFlushed := ProtocolEvent(&ChannelFlushed{
|
channelFlushed := ProtocolEvent(&ChannelFlushed{
|
||||||
ShutdownBalances: finalBalances,
|
ShutdownBalances: finalBalances,
|
||||||
})
|
})
|
||||||
eventsToEmit = fn.Some(RbfEvent{
|
eventsToEmit = append(eventsToEmit, channelFlushed)
|
||||||
InternalEvent: []ProtocolEvent{
|
}
|
||||||
channelFlushed,
|
|
||||||
},
|
// If we received a remote offer early from the remote party,
|
||||||
|
// then we'll add that to the set of internal events to emit.
|
||||||
|
s.EarlyRemoteOffer.WhenSome(func(offer OfferReceivedEvent) {
|
||||||
|
eventsToEmit = append(eventsToEmit, &offer)
|
||||||
|
})
|
||||||
|
|
||||||
|
var newEvents fn.Option[RbfEvent]
|
||||||
|
if len(eventsToEmit) > 0 {
|
||||||
|
newEvents = fn.Some(RbfEvent{
|
||||||
|
InternalEvent: eventsToEmit,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,7 +427,7 @@ func (s *ShutdownPending) ProcessEvent(event ProtocolEvent, env *Environment,
|
|||||||
IdealFeeRate: s.IdealFeeRate,
|
IdealFeeRate: s.IdealFeeRate,
|
||||||
ShutdownScripts: s.ShutdownScripts,
|
ShutdownScripts: s.ShutdownScripts,
|
||||||
},
|
},
|
||||||
NewEvents: eventsToEmit,
|
NewEvents: newEvents,
|
||||||
}, nil
|
}, nil
|
||||||
|
|
||||||
// Any other messages in this state will result in an error, as this is
|
// Any other messages in this state will result in an error, as this is
|
||||||
@@ -423,9 +467,6 @@ func (c *ChannelFlushing) ProcessEvent(event ProtocolEvent, env *Environment,
|
|||||||
|
|
||||||
c.EarlyRemoteOffer = fn.Some(*msg)
|
c.EarlyRemoteOffer = fn.Some(*msg)
|
||||||
|
|
||||||
// TODO(roasbeef): unit test!
|
|
||||||
// * actually do this ^
|
|
||||||
|
|
||||||
// We'll perform a noop update so we can wait for the actual
|
// We'll perform a noop update so we can wait for the actual
|
||||||
// channel flushed event.
|
// channel flushed event.
|
||||||
return &CloseStateTransition{
|
return &CloseStateTransition{
|
||||||
@@ -466,8 +507,6 @@ func (c *ChannelFlushing) ProcessEvent(event ProtocolEvent, env *Environment,
|
|||||||
|
|
||||||
// We'll then use that fee rate to determine the absolute fee
|
// We'll then use that fee rate to determine the absolute fee
|
||||||
// we'd propose.
|
// we'd propose.
|
||||||
//
|
|
||||||
// TODO(roasbeef): need to sign the 3 diff versions of this?
|
|
||||||
localTxOut, remoteTxOut := closeTerms.DeriveCloseTxOuts()
|
localTxOut, remoteTxOut := closeTerms.DeriveCloseTxOuts()
|
||||||
absoluteFee := env.FeeEstimator.EstimateFee(
|
absoluteFee := env.FeeEstimator.EstimateFee(
|
||||||
env.ChanType, localTxOut, remoteTxOut,
|
env.ChanType, localTxOut, remoteTxOut,
|
||||||
@@ -519,12 +558,13 @@ func (c *ChannelFlushing) ProcessEvent(event ProtocolEvent, env *Environment,
|
|||||||
NextState: &ClosingNegotiation{
|
NextState: &ClosingNegotiation{
|
||||||
PeerState: lntypes.Dual[AsymmetricPeerState]{
|
PeerState: lntypes.Dual[AsymmetricPeerState]{
|
||||||
Local: &LocalCloseStart{
|
Local: &LocalCloseStart{
|
||||||
CloseChannelTerms: closeTerms,
|
CloseChannelTerms: &closeTerms,
|
||||||
},
|
},
|
||||||
Remote: &RemoteCloseStart{
|
Remote: &RemoteCloseStart{
|
||||||
CloseChannelTerms: closeTerms,
|
CloseChannelTerms: &closeTerms,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
CloseChannelTerms: &closeTerms,
|
||||||
},
|
},
|
||||||
NewEvents: newEvents,
|
NewEvents: newEvents,
|
||||||
}, nil
|
}, nil
|
||||||
@@ -568,6 +608,63 @@ func processNegotiateEvent(c *ClosingNegotiation, event ProtocolEvent,
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateAndValidateCloseTerms is a helper function that validates examines the
|
||||||
|
// incoming event, and decide if we need to update the remote party's address,
|
||||||
|
// or reject it if it doesn't include our latest address.
|
||||||
|
func (c *ClosingNegotiation) updateAndValidateCloseTerms(event ProtocolEvent,
|
||||||
|
) error {
|
||||||
|
|
||||||
|
assertLocalScriptMatches := func(localScriptInMsg []byte) error {
|
||||||
|
if !bytes.Equal(
|
||||||
|
c.LocalDeliveryScript, localScriptInMsg,
|
||||||
|
) {
|
||||||
|
|
||||||
|
return fmt.Errorf("%w: remote party sent wrong "+
|
||||||
|
"script, expected %x, got %x",
|
||||||
|
ErrWrongLocalScript, c.LocalDeliveryScript,
|
||||||
|
localScriptInMsg,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch msg := event.(type) {
|
||||||
|
// The remote party is sending us a new request to counter sign their
|
||||||
|
// version of the commitment transaction.
|
||||||
|
case *OfferReceivedEvent:
|
||||||
|
// Make sure that they're sending our local script, and not
|
||||||
|
// something else.
|
||||||
|
err := assertLocalScriptMatches(msg.SigMsg.CloseeScript)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
oldRemoteAddr := c.RemoteDeliveryScript
|
||||||
|
newRemoteAddr := msg.SigMsg.CloserScript
|
||||||
|
|
||||||
|
// If they're sending a new script, then we'll update to the new
|
||||||
|
// one.
|
||||||
|
if !bytes.Equal(oldRemoteAddr, newRemoteAddr) {
|
||||||
|
c.RemoteDeliveryScript = newRemoteAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
// The remote party responded to our sig request with a signature for
|
||||||
|
// our version of the commitment transaction.
|
||||||
|
case *LocalSigReceived:
|
||||||
|
// Make sure that they're sending our local script, and not
|
||||||
|
// something else.
|
||||||
|
err := assertLocalScriptMatches(msg.SigMsg.CloserScript)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ProcessEvent drives forward the composite states for the local and remote
|
// ProcessEvent drives forward the composite states for the local and remote
|
||||||
// party in response to new events. From this state, we'll continue to drive
|
// party in response to new events. From this state, we'll continue to drive
|
||||||
// forward the local and remote states until we arrive at the StateFin stage,
|
// forward the local and remote states until we arrive at the StateFin stage,
|
||||||
@@ -579,6 +676,12 @@ func (c *ClosingNegotiation) ProcessEvent(event ProtocolEvent, env *Environment,
|
|||||||
// we receive a confirmation event, or we receive a signal to restart
|
// we receive a confirmation event, or we receive a signal to restart
|
||||||
// the co-op close process.
|
// the co-op close process.
|
||||||
switch msg := event.(type) {
|
switch msg := event.(type) {
|
||||||
|
// Ignore any potential duplicate channel flushed events.
|
||||||
|
case *ChannelFlushed:
|
||||||
|
return &CloseStateTransition{
|
||||||
|
NextState: c,
|
||||||
|
}, nil
|
||||||
|
|
||||||
// If we get a confirmation, then the spend request we issued when we
|
// If we get a confirmation, then the spend request we issued when we
|
||||||
// were leaving the ChannelFlushing state has been confirmed. We'll
|
// were leaving the ChannelFlushing state has been confirmed. We'll
|
||||||
// now transition to the StateFin state.
|
// now transition to the StateFin state.
|
||||||
@@ -588,45 +691,45 @@ func (c *ClosingNegotiation) ProcessEvent(event ProtocolEvent, env *Environment,
|
|||||||
ConfirmedTx: msg.Tx,
|
ConfirmedTx: msg.Tx,
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Otherwise, if we receive a shutdown, or receive an event to send a
|
// At this point, we know its a new signature message. We'll validate,
|
||||||
// shutdown, then we'll go back up to the ChannelActive state, and have
|
// and maybe update the set of close terms based on what we receive. We
|
||||||
// it handle this event by emitting an internal event.
|
// might update the remote party's address for example.
|
||||||
//
|
if err := c.updateAndValidateCloseTerms(event); err != nil {
|
||||||
// TODO(roasbeef): both will have fee rate specified, so ok?
|
return nil, fmt.Errorf("event violates close terms: %w", err)
|
||||||
case *ShutdownReceived, *SendShutdown:
|
}
|
||||||
chancloserLog.Infof("ChannelPoint(%v): RBF case triggered, "+
|
|
||||||
"restarting negotiation", env.ChanPoint)
|
|
||||||
|
|
||||||
return &CloseStateTransition{
|
shouldRouteTo := func(party lntypes.ChannelParty) bool {
|
||||||
NextState: &ChannelActive{},
|
state := c.PeerState.GetForParty(party)
|
||||||
NewEvents: fn.Some(RbfEvent{
|
if state == nil {
|
||||||
InternalEvent: []ProtocolEvent{event},
|
return false
|
||||||
}),
|
}
|
||||||
}, nil
|
|
||||||
|
return state.ShouldRouteTo(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we get to this point, then we have an event that'll drive forward
|
// If we get to this point, then we have an event that'll drive forward
|
||||||
// the negotiation process. Based on the event, we'll figure out which
|
// the negotiation process. Based on the event, we'll figure out which
|
||||||
// state we'll be modifying.
|
// state we'll be modifying.
|
||||||
switch {
|
switch {
|
||||||
case c.PeerState.GetForParty(lntypes.Local).ShouldRouteTo(event):
|
case shouldRouteTo(lntypes.Local):
|
||||||
chancloserLog.Infof("ChannelPoint(%v): routing %T to local "+
|
chancloserLog.Infof("ChannelPoint(%v): routing %T to local "+
|
||||||
"chan state", env.ChanPoint, event)
|
"chan state", env.ChanPoint, event)
|
||||||
|
|
||||||
// Drive forward the local state based on the next event.
|
// Drive forward the local state based on the next event.
|
||||||
return processNegotiateEvent(c, event, env, lntypes.Local)
|
return processNegotiateEvent(c, event, env, lntypes.Local)
|
||||||
|
|
||||||
case c.PeerState.GetForParty(lntypes.Remote).ShouldRouteTo(event):
|
case shouldRouteTo(lntypes.Remote):
|
||||||
chancloserLog.Infof("ChannelPoint(%v): routing %T to remote "+
|
chancloserLog.Infof("ChannelPoint(%v): routing %T to remote "+
|
||||||
"chan state", env.ChanPoint, event)
|
|
||||||
|
|
||||||
|
"chan state", env.ChanPoint, event)
|
||||||
// Drive forward the remote state based on the next event.
|
// Drive forward the remote state based on the next event.
|
||||||
return processNegotiateEvent(c, event, env, lntypes.Remote)
|
return processNegotiateEvent(c, event, env, lntypes.Remote)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf("%w: received %T while in ClosingNegotiation",
|
return nil, fmt.Errorf("%w: received %T while in %v",
|
||||||
ErrInvalidStateTransition, event)
|
ErrInvalidStateTransition, event, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// newSigTlv is a helper function that returns a new optional TLV sig field for
|
// newSigTlv is a helper function that returns a new optional TLV sig field for
|
||||||
@@ -645,14 +748,34 @@ func (l *LocalCloseStart) ProcessEvent(event ProtocolEvent, env *Environment,
|
|||||||
// rate to generate for the closing transaction with our ideal fee
|
// rate to generate for the closing transaction with our ideal fee
|
||||||
// rate.
|
// rate.
|
||||||
case *SendOfferEvent:
|
case *SendOfferEvent:
|
||||||
// First, we'll figure out the absolute fee rate we should pay
|
|
||||||
// given the state of the local/remote outputs.
|
// given the state of the local/remote outputs.
|
||||||
|
// First, we'll figure out the absolute fee rate we should pay
|
||||||
localTxOut, remoteTxOut := l.DeriveCloseTxOuts()
|
localTxOut, remoteTxOut := l.DeriveCloseTxOuts()
|
||||||
absoluteFee := env.FeeEstimator.EstimateFee(
|
absoluteFee := env.FeeEstimator.EstimateFee(
|
||||||
env.ChanType, localTxOut, remoteTxOut,
|
env.ChanType, localTxOut, remoteTxOut,
|
||||||
msg.TargetFeeRate.FeePerKWeight(),
|
msg.TargetFeeRate.FeePerKWeight(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// If we can't actually pay for fees here, then we'll just do a
|
||||||
|
// noop back to the same state to await a new fee rate.
|
||||||
|
if !l.LocalCanPayFees(absoluteFee) {
|
||||||
|
chancloserLog.Infof("ChannelPoint(%v): unable to pay "+
|
||||||
|
"fee=%v with local balance %v, skipping "+
|
||||||
|
"closing_complete", env.ChanPoint, absoluteFee,
|
||||||
|
l.LocalBalance)
|
||||||
|
|
||||||
|
return &CloseStateTransition{
|
||||||
|
NextState: &CloseErr{
|
||||||
|
CloseChannelTerms: l.CloseChannelTerms,
|
||||||
|
Party: lntypes.Local,
|
||||||
|
ErrState: NewErrStateCantPayForFee(
|
||||||
|
l.LocalBalance.ToSatoshis(),
|
||||||
|
absoluteFee,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Now that we know what fee we want to pay, we'll create a new
|
// Now that we know what fee we want to pay, we'll create a new
|
||||||
// signature over our co-op close transaction. For our
|
// signature over our co-op close transaction. For our
|
||||||
// proposals, we'll just always use the known RBF sequence
|
// proposals, we'll just always use the known RBF sequence
|
||||||
@@ -713,9 +836,10 @@ func (l *LocalCloseStart) ProcessEvent(event ProtocolEvent, env *Environment,
|
|||||||
// TODO(roasbeef): type alias for protocol event
|
// TODO(roasbeef): type alias for protocol event
|
||||||
sendEvent := protofsm.DaemonEventSet{&protofsm.SendMsgEvent[ProtocolEvent]{ //nolint:ll
|
sendEvent := protofsm.DaemonEventSet{&protofsm.SendMsgEvent[ProtocolEvent]{ //nolint:ll
|
||||||
TargetPeer: env.ChanPeer,
|
TargetPeer: env.ChanPeer,
|
||||||
// TODO(roasbeef): mew new func
|
|
||||||
Msgs: []lnwire.Message{&lnwire.ClosingComplete{
|
Msgs: []lnwire.Message{&lnwire.ClosingComplete{
|
||||||
ChannelID: env.ChanID,
|
ChannelID: env.ChanID,
|
||||||
|
CloserScript: l.LocalDeliveryScript,
|
||||||
|
CloseeScript: l.RemoteDeliveryScript,
|
||||||
FeeSatoshis: absoluteFee,
|
FeeSatoshis: absoluteFee,
|
||||||
LockTime: env.BlockHeight,
|
LockTime: env.BlockHeight,
|
||||||
ClosingSigs: closingSigs,
|
ClosingSigs: closingSigs,
|
||||||
@@ -729,6 +853,7 @@ func (l *LocalCloseStart) ProcessEvent(event ProtocolEvent, env *Environment,
|
|||||||
return &CloseStateTransition{
|
return &CloseStateTransition{
|
||||||
NextState: &LocalOfferSent{
|
NextState: &LocalOfferSent{
|
||||||
ProposedFee: absoluteFee,
|
ProposedFee: absoluteFee,
|
||||||
|
ProposedFeeRate: msg.TargetFeeRate,
|
||||||
LocalSig: wireSig,
|
LocalSig: wireSig,
|
||||||
CloseChannelTerms: l.CloseChannelTerms,
|
CloseChannelTerms: l.CloseChannelTerms,
|
||||||
},
|
},
|
||||||
@@ -838,6 +963,9 @@ func (l *LocalOfferSent) ProcessEvent(event ProtocolEvent, env *Environment,
|
|||||||
return &CloseStateTransition{
|
return &CloseStateTransition{
|
||||||
NextState: &ClosePending{
|
NextState: &ClosePending{
|
||||||
CloseTx: closeTx,
|
CloseTx: closeTx,
|
||||||
|
FeeRate: l.ProposedFeeRate,
|
||||||
|
CloseChannelTerms: l.CloseChannelTerms,
|
||||||
|
Party: lntypes.Local,
|
||||||
},
|
},
|
||||||
NewEvents: fn.Some(protofsm.EmittedEvent[ProtocolEvent]{
|
NewEvents: fn.Some(protofsm.EmittedEvent[ProtocolEvent]{
|
||||||
ExternalEvents: broadcastEvent,
|
ExternalEvents: broadcastEvent,
|
||||||
@@ -981,6 +1109,10 @@ func (l *RemoteCloseStart) ProcessEvent(event ProtocolEvent, env *Environment,
|
|||||||
TargetPeer: env.ChanPeer,
|
TargetPeer: env.ChanPeer,
|
||||||
Msgs: []lnwire.Message{&lnwire.ClosingSig{
|
Msgs: []lnwire.Message{&lnwire.ClosingSig{
|
||||||
ChannelID: env.ChanID,
|
ChannelID: env.ChanID,
|
||||||
|
CloserScript: l.RemoteDeliveryScript,
|
||||||
|
CloseeScript: l.LocalDeliveryScript,
|
||||||
|
FeeSatoshis: msg.SigMsg.FeeSatoshis,
|
||||||
|
LockTime: msg.SigMsg.LockTime,
|
||||||
ClosingSigs: closingSigs,
|
ClosingSigs: closingSigs,
|
||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
@@ -994,11 +1126,22 @@ func (l *RemoteCloseStart) ProcessEvent(event ProtocolEvent, env *Environment,
|
|||||||
sendEvent, broadcastEvent,
|
sendEvent, broadcastEvent,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We'll also compute the final fee rate that the remote party
|
||||||
|
// paid based off the absolute fee and the size of the closing
|
||||||
|
// transaction.
|
||||||
|
vSize := mempool.GetTxVirtualSize(btcutil.NewTx(closeTx))
|
||||||
|
feeRate := chainfee.SatPerVByte(
|
||||||
|
int64(msg.SigMsg.FeeSatoshis) / vSize,
|
||||||
|
)
|
||||||
|
|
||||||
// Now that we've extracted the signature, we'll transition to
|
// Now that we've extracted the signature, we'll transition to
|
||||||
// the next state where we'll sign+broadcast the sig.
|
// the next state where we'll sign+broadcast the sig.
|
||||||
return &CloseStateTransition{
|
return &CloseStateTransition{
|
||||||
NextState: &ClosePending{
|
NextState: &ClosePending{
|
||||||
CloseTx: closeTx,
|
CloseTx: closeTx,
|
||||||
|
FeeRate: feeRate,
|
||||||
|
CloseChannelTerms: l.CloseChannelTerms,
|
||||||
|
Party: lntypes.Remote,
|
||||||
},
|
},
|
||||||
NewEvents: fn.Some(protofsm.EmittedEvent[ProtocolEvent]{
|
NewEvents: fn.Some(protofsm.EmittedEvent[ProtocolEvent]{
|
||||||
ExternalEvents: daemonEvents,
|
ExternalEvents: daemonEvents,
|
||||||
@@ -1026,6 +1169,32 @@ func (c *ClosePending) ProcessEvent(event ProtocolEvent, env *Environment,
|
|||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
|
|
||||||
|
// If we get a send offer event in this state, then we're doing a state
|
||||||
|
// transition to the LocalCloseStart state, so we can sign a new closing
|
||||||
|
// tx.
|
||||||
|
case *SendOfferEvent:
|
||||||
|
return &CloseStateTransition{
|
||||||
|
NextState: &LocalCloseStart{
|
||||||
|
CloseChannelTerms: c.CloseChannelTerms,
|
||||||
|
},
|
||||||
|
NewEvents: fn.Some(protofsm.EmittedEvent[ProtocolEvent]{
|
||||||
|
InternalEvent: []ProtocolEvent{msg},
|
||||||
|
}),
|
||||||
|
}, nil
|
||||||
|
|
||||||
|
// If we get an offer received event, then we're doing a state
|
||||||
|
// transition to the RemoteCloseStart, as the remote peer wants to sign
|
||||||
|
// a new closing tx.
|
||||||
|
case *OfferReceivedEvent:
|
||||||
|
return &CloseStateTransition{
|
||||||
|
NextState: &RemoteCloseStart{
|
||||||
|
CloseChannelTerms: c.CloseChannelTerms,
|
||||||
|
},
|
||||||
|
NewEvents: fn.Some(protofsm.EmittedEvent[ProtocolEvent]{
|
||||||
|
InternalEvent: []ProtocolEvent{msg},
|
||||||
|
}),
|
||||||
|
}, nil
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|
||||||
return &CloseStateTransition{
|
return &CloseStateTransition{
|
||||||
@@ -1043,3 +1212,44 @@ func (c *CloseFin) ProcessEvent(event ProtocolEvent, env *Environment,
|
|||||||
NextState: c,
|
NextState: c,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProcessEvent is a semi-terminal state in the rbf-coop close state machine.
|
||||||
|
// In this state, we hit a validation error in an earlier state, so we'll remain
|
||||||
|
// in this state for the user to examine. We may also process new requests to
|
||||||
|
// continue the state machine.
|
||||||
|
func (c *CloseErr) ProcessEvent(event ProtocolEvent, env *Environment,
|
||||||
|
) (*CloseStateTransition, error) {
|
||||||
|
|
||||||
|
switch msg := event.(type) {
|
||||||
|
// If we get a send offer event in this state, then we're doing a state
|
||||||
|
// transition to the LocalCloseStart state, so we can sign a new closing
|
||||||
|
// tx.
|
||||||
|
case *SendOfferEvent:
|
||||||
|
return &CloseStateTransition{
|
||||||
|
NextState: &LocalCloseStart{
|
||||||
|
CloseChannelTerms: c.CloseChannelTerms,
|
||||||
|
},
|
||||||
|
NewEvents: fn.Some(protofsm.EmittedEvent[ProtocolEvent]{
|
||||||
|
InternalEvent: []ProtocolEvent{msg},
|
||||||
|
}),
|
||||||
|
}, nil
|
||||||
|
|
||||||
|
// If we get an offer received event, then we're doing a state
|
||||||
|
// transition to the RemoteCloseStart, as the remote peer wants to sign
|
||||||
|
// a new closing tx.
|
||||||
|
case *OfferReceivedEvent:
|
||||||
|
return &CloseStateTransition{
|
||||||
|
NextState: &RemoteCloseStart{
|
||||||
|
CloseChannelTerms: c.CloseChannelTerms,
|
||||||
|
},
|
||||||
|
NewEvents: fn.Some(protofsm.EmittedEvent[ProtocolEvent]{
|
||||||
|
InternalEvent: []ProtocolEvent{msg},
|
||||||
|
}),
|
||||||
|
}, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return &CloseStateTransition{
|
||||||
|
NextState: c,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
+39
-4
@@ -9269,13 +9269,19 @@ func CreateCooperativeCloseTx(fundingTxIn wire.TxIn,
|
|||||||
closeTx.LockTime = lockTime
|
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]
|
var localOutputIdx fn.Option[int]
|
||||||
haveLocalOutput := ourBalance >= localDust
|
haveLocalOutput := ourBalance >= localDust
|
||||||
if haveLocalOutput {
|
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{
|
closeTx.AddTxOut(&wire.TxOut{
|
||||||
PkScript: ourDeliveryScript,
|
PkScript: ourDeliveryScript,
|
||||||
Value: int64(ourBalance),
|
Value: int64(ourBalance),
|
||||||
@@ -9287,6 +9293,14 @@ func CreateCooperativeCloseTx(fundingTxIn wire.TxIn,
|
|||||||
var remoteOutputIdx fn.Option[int]
|
var remoteOutputIdx fn.Option[int]
|
||||||
haveRemoteOutput := theirBalance >= remoteDust
|
haveRemoteOutput := theirBalance >= remoteDust
|
||||||
if haveRemoteOutput {
|
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{
|
closeTx.AddTxOut(&wire.TxOut{
|
||||||
PkScript: theirDeliveryScript,
|
PkScript: theirDeliveryScript,
|
||||||
Value: int64(theirBalance),
|
Value: int64(theirBalance),
|
||||||
@@ -9874,6 +9888,14 @@ func (lc *LightningChannel) FundingTxOut() *wire.TxOut {
|
|||||||
return &lc.fundingOutput
|
return &lc.fundingOutput
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeriveHeightHint derives the block height for the channel opening.
|
||||||
|
func (lc *LightningChannel) DeriveHeightHint() uint32 {
|
||||||
|
lc.RLock()
|
||||||
|
defer lc.RUnlock()
|
||||||
|
|
||||||
|
return lc.channelState.DeriveHeightHint()
|
||||||
|
}
|
||||||
|
|
||||||
// MultiSigKeys returns the set of multi-sig keys for an channel.
|
// MultiSigKeys returns the set of multi-sig keys for an channel.
|
||||||
func (lc *LightningChannel) MultiSigKeys() (keychain.KeyDescriptor,
|
func (lc *LightningChannel) MultiSigKeys() (keychain.KeyDescriptor,
|
||||||
keychain.KeyDescriptor) {
|
keychain.KeyDescriptor) {
|
||||||
@@ -9913,3 +9935,16 @@ func (lc *LightningChannel) FundingBlob() fn.Option[tlv.Blob] {
|
|||||||
return newBlob
|
return newBlob
|
||||||
})(lc.channelState.CustomBlob)
|
})(lc.channelState.CustomBlob)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ZeroConfRealScid returns an optional real scid for the channel. If this
|
||||||
|
// returns None, then this isn't a zero conf channel. Otherwise, the real scid
|
||||||
|
// value will be returned.
|
||||||
|
//
|
||||||
|
//nolint:ll
|
||||||
|
func (lc *LightningChannel) ZeroConfRealScid() fn.Option[lnwire.ShortChannelID] {
|
||||||
|
if lc.channelState.IsZeroConf() {
|
||||||
|
return fn.Some(lc.channelState.ZeroConfRealScid())
|
||||||
|
}
|
||||||
|
|
||||||
|
return fn.None[lnwire.ShortChannelID]()
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
"github.com/btcsuite/btcd/btcutil"
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
"github.com/btcsuite/btcd/btcutil/txsort"
|
"github.com/btcsuite/btcd/btcutil/txsort"
|
||||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
|
"github.com/btcsuite/btcd/mempool"
|
||||||
"github.com/btcsuite/btcd/txscript"
|
"github.com/btcsuite/btcd/txscript"
|
||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
"github.com/davecgh/go-spew/spew"
|
"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
|
// TestUpdateFeeAdjustments tests that the state machine is able to properly
|
||||||
// accept valid fee changes, as well as reject any invalid fee updates.
|
// accept valid fee changes, as well as reject any invalid fee updates.
|
||||||
func TestUpdateFeeAdjustments(t *testing.T) {
|
func TestUpdateFeeAdjustments(t *testing.T) {
|
||||||
|
|||||||
@@ -30,6 +30,14 @@ type ClosingComplete struct {
|
|||||||
// ChannelID serves to identify which channel is to be closed.
|
// ChannelID serves to identify which channel is to be closed.
|
||||||
ChannelID ChannelID
|
ChannelID ChannelID
|
||||||
|
|
||||||
|
// CloserScript is the script to which the channel funds will be paid
|
||||||
|
// for the closer (the person sending the ClosingComplete) message.
|
||||||
|
CloserScript DeliveryAddress
|
||||||
|
|
||||||
|
// CloseeScript is the script to which the channel funds will be paid
|
||||||
|
// (the person receiving the ClosingComplete message).
|
||||||
|
CloseeScript DeliveryAddress
|
||||||
|
|
||||||
// FeeSatoshis is the total fee in satoshis that the party to the
|
// FeeSatoshis is the total fee in satoshis that the party to the
|
||||||
// channel would like to propose for the close transaction.
|
// channel would like to propose for the close transaction.
|
||||||
FeeSatoshis btcutil.Amount
|
FeeSatoshis btcutil.Amount
|
||||||
@@ -79,7 +87,10 @@ func decodeClosingSigs(c *ClosingSigs, tlvRecords ExtraOpaqueData) error {
|
|||||||
// passed io.Reader.
|
// passed io.Reader.
|
||||||
func (c *ClosingComplete) Decode(r io.Reader, _ uint32) error {
|
func (c *ClosingComplete) Decode(r io.Reader, _ uint32) error {
|
||||||
// First, read out all the fields that are hard coded into the message.
|
// First, read out all the fields that are hard coded into the message.
|
||||||
err := ReadElements(r, &c.ChannelID, &c.FeeSatoshis, &c.LockTime)
|
err := ReadElements(
|
||||||
|
r, &c.ChannelID, &c.CloserScript, &c.CloseeScript,
|
||||||
|
&c.FeeSatoshis, &c.LockTime,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -125,6 +136,13 @@ func (c *ClosingComplete) Encode(w *bytes.Buffer, _ uint32) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := WriteDeliveryAddress(w, c.CloserScript); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := WriteDeliveryAddress(w, c.CloseeScript); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if err := WriteSatoshi(w, c.FeeSatoshis); err != nil {
|
if err := WriteSatoshi(w, c.FeeSatoshis); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
+37
-1
@@ -3,6 +3,8 @@ package lnwire
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ClosingSig is sent in response to a ClosingComplete message. It carries the
|
// ClosingSig is sent in response to a ClosingComplete message. It carries the
|
||||||
@@ -11,6 +13,22 @@ type ClosingSig struct {
|
|||||||
// ChannelID serves to identify which channel is to be closed.
|
// ChannelID serves to identify which channel is to be closed.
|
||||||
ChannelID ChannelID
|
ChannelID ChannelID
|
||||||
|
|
||||||
|
// CloserScript is the script to which the channel funds will be paid
|
||||||
|
// for the closer (the person sending the ClosingComplete) message.
|
||||||
|
CloserScript DeliveryAddress
|
||||||
|
|
||||||
|
// CloseeScript is the script to which the channel funds will be paid
|
||||||
|
// (the person receiving the ClosingComplete message).
|
||||||
|
CloseeScript DeliveryAddress
|
||||||
|
|
||||||
|
// FeeSatoshis is the total fee in satoshis that the party to the
|
||||||
|
// channel proposed for the close transaction.
|
||||||
|
FeeSatoshis btcutil.Amount
|
||||||
|
|
||||||
|
// LockTime is the locktime number to be used in the input spending the
|
||||||
|
// funding transaction.
|
||||||
|
LockTime uint32
|
||||||
|
|
||||||
// ClosingSigs houses the 3 possible signatures that can be sent.
|
// ClosingSigs houses the 3 possible signatures that can be sent.
|
||||||
ClosingSigs
|
ClosingSigs
|
||||||
|
|
||||||
@@ -24,7 +42,10 @@ type ClosingSig struct {
|
|||||||
// io.Reader.
|
// io.Reader.
|
||||||
func (c *ClosingSig) Decode(r io.Reader, _ uint32) error {
|
func (c *ClosingSig) Decode(r io.Reader, _ uint32) error {
|
||||||
// First, read out all the fields that are hard coded into the message.
|
// First, read out all the fields that are hard coded into the message.
|
||||||
err := ReadElements(r, &c.ChannelID)
|
err := ReadElements(
|
||||||
|
r, &c.ChannelID, &c.CloserScript, &c.CloseeScript,
|
||||||
|
&c.FeeSatoshis, &c.LockTime,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -53,6 +74,21 @@ func (c *ClosingSig) Encode(w *bytes.Buffer, _ uint32) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := WriteDeliveryAddress(w, c.CloserScript); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := WriteDeliveryAddress(w, c.CloseeScript); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := WriteSatoshi(w, c.FeeSatoshis); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := WriteUint32(w, c.LockTime); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
recordProducers := closingSigRecords(&c.ClosingSigs)
|
recordProducers := closingSigRecords(&c.ClosingSigs)
|
||||||
|
|
||||||
err := EncodeMessageExtraData(&c.ExtraData, recordProducers...)
|
err := EncodeMessageExtraData(&c.ExtraData, recordProducers...)
|
||||||
|
|||||||
@@ -233,6 +233,22 @@ const (
|
|||||||
// able and willing to accept keysend payments.
|
// able and willing to accept keysend payments.
|
||||||
KeysendOptional = 55
|
KeysendOptional = 55
|
||||||
|
|
||||||
|
// RbfCoopCloseRequired is a required feature bit that signals that
|
||||||
|
// the new RBF-based co-op close protocol is supported.
|
||||||
|
RbfCoopCloseRequired = 60
|
||||||
|
|
||||||
|
// RbfCoopCloseOptional is an optional feature bit that signals that the
|
||||||
|
// new RBF-based co-op close protocol is supported.
|
||||||
|
RbfCoopCloseOptional = 61
|
||||||
|
|
||||||
|
// RbfCoopCloseRequiredStaging is a required feature bit that signals
|
||||||
|
// that the new RBF-based co-op close protocol is supported.
|
||||||
|
RbfCoopCloseRequiredStaging = 160
|
||||||
|
|
||||||
|
// RbfCoopCloseOptionalStaging is an optional feature bit that signals
|
||||||
|
// that the new RBF-based co-op close protocol is supported.
|
||||||
|
RbfCoopCloseOptionalStaging = 161
|
||||||
|
|
||||||
// ScriptEnforcedLeaseRequired is a required feature bit that signals
|
// ScriptEnforcedLeaseRequired is a required feature bit that signals
|
||||||
// that the node requires channels having zero-fee second-level HTLC
|
// that the node requires channels having zero-fee second-level HTLC
|
||||||
// transactions, which also imply anchor commitments, along with an
|
// transactions, which also imply anchor commitments, along with an
|
||||||
@@ -373,6 +389,10 @@ var Features = map[FeatureBit]string{
|
|||||||
ExperimentalEndorsementOptional: "endorsement-x",
|
ExperimentalEndorsementOptional: "endorsement-x",
|
||||||
Bolt11BlindedPathsOptional: "bolt-11-blinded-paths",
|
Bolt11BlindedPathsOptional: "bolt-11-blinded-paths",
|
||||||
Bolt11BlindedPathsRequired: "bolt-11-blinded-paths",
|
Bolt11BlindedPathsRequired: "bolt-11-blinded-paths",
|
||||||
|
RbfCoopCloseOptional: "rbf-coop-close",
|
||||||
|
RbfCoopCloseRequired: "rbf-coop-close",
|
||||||
|
RbfCoopCloseOptionalStaging: "rbf-coop-close-x",
|
||||||
|
RbfCoopCloseRequiredStaging: "rbf-coop-close-x",
|
||||||
}
|
}
|
||||||
|
|
||||||
// RawFeatureVector represents a set of feature bits as defined in BOLT-09. A
|
// RawFeatureVector represents a set of feature bits as defined in BOLT-09. A
|
||||||
|
|||||||
@@ -1355,6 +1355,18 @@ func TestLightningWireProtocol(t *testing.T) {
|
|||||||
LockTime: uint32(r.Int63()),
|
LockTime: uint32(r.Int63()),
|
||||||
ClosingSigs: ClosingSigs{},
|
ClosingSigs: ClosingSigs{},
|
||||||
}
|
}
|
||||||
|
req.CloserScript, err = randDeliveryAddress(r)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to generate delivery "+
|
||||||
|
"address: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.CloseeScript, err = randDeliveryAddress(r)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to generate delivery "+
|
||||||
|
"address: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if r.Intn(2) == 0 {
|
if r.Intn(2) == 0 {
|
||||||
sig := req.CloserNoClosee.Zero()
|
sig := req.CloserNoClosee.Zero()
|
||||||
@@ -1403,6 +1415,20 @@ func TestLightningWireProtocol(t *testing.T) {
|
|||||||
req := ClosingSig{
|
req := ClosingSig{
|
||||||
ChannelID: ChannelID(c),
|
ChannelID: ChannelID(c),
|
||||||
ClosingSigs: ClosingSigs{},
|
ClosingSigs: ClosingSigs{},
|
||||||
|
FeeSatoshis: btcutil.Amount(r.Int63()),
|
||||||
|
LockTime: uint32(r.Int63()),
|
||||||
|
}
|
||||||
|
req.CloserScript, err = randDeliveryAddress(r)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to generate delivery "+
|
||||||
|
"address: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.CloseeScript, err = randDeliveryAddress(r)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to generate delivery "+
|
||||||
|
"address: %v", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.Intn(2) == 0 {
|
if r.Intn(2) == 0 {
|
||||||
|
|||||||
@@ -44,9 +44,11 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/lnwallet/chanfunding"
|
"github.com/lightningnetwork/lnd/lnwallet/chanfunding"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet/rpcwallet"
|
"github.com/lightningnetwork/lnd/lnwallet/rpcwallet"
|
||||||
"github.com/lightningnetwork/lnd/monitoring"
|
"github.com/lightningnetwork/lnd/monitoring"
|
||||||
|
"github.com/lightningnetwork/lnd/msgmux"
|
||||||
"github.com/lightningnetwork/lnd/netann"
|
"github.com/lightningnetwork/lnd/netann"
|
||||||
"github.com/lightningnetwork/lnd/peer"
|
"github.com/lightningnetwork/lnd/peer"
|
||||||
"github.com/lightningnetwork/lnd/peernotifier"
|
"github.com/lightningnetwork/lnd/peernotifier"
|
||||||
|
"github.com/lightningnetwork/lnd/protofsm"
|
||||||
"github.com/lightningnetwork/lnd/routing"
|
"github.com/lightningnetwork/lnd/routing"
|
||||||
"github.com/lightningnetwork/lnd/routing/blindedpath"
|
"github.com/lightningnetwork/lnd/routing/blindedpath"
|
||||||
"github.com/lightningnetwork/lnd/routing/localchans"
|
"github.com/lightningnetwork/lnd/routing/localchans"
|
||||||
@@ -178,6 +180,7 @@ func SetupLoggers(root *build.SubLoggerManager, interceptor signal.Interceptor)
|
|||||||
AddSubLogger(root, "PEER", interceptor, peer.UseLogger)
|
AddSubLogger(root, "PEER", interceptor, peer.UseLogger)
|
||||||
AddSubLogger(root, "CHCL", interceptor, chancloser.UseLogger)
|
AddSubLogger(root, "CHCL", interceptor, chancloser.UseLogger)
|
||||||
AddSubLogger(root, "LCHN", interceptor, localchans.UseLogger)
|
AddSubLogger(root, "LCHN", interceptor, localchans.UseLogger)
|
||||||
|
AddSubLogger(root, "PFSM", interceptor, protofsm.UseLogger)
|
||||||
|
|
||||||
AddSubLogger(root, routing.Subsystem, interceptor, routing.UseLogger)
|
AddSubLogger(root, routing.Subsystem, interceptor, routing.UseLogger)
|
||||||
AddSubLogger(root, routerrpc.Subsystem, interceptor, routerrpc.UseLogger)
|
AddSubLogger(root, routerrpc.Subsystem, interceptor, routerrpc.UseLogger)
|
||||||
@@ -200,6 +203,7 @@ func SetupLoggers(root *build.SubLoggerManager, interceptor signal.Interceptor)
|
|||||||
)
|
)
|
||||||
AddV1SubLogger(root, graphdb.Subsystem, interceptor, graphdb.UseLogger)
|
AddV1SubLogger(root, graphdb.Subsystem, interceptor, graphdb.UseLogger)
|
||||||
AddSubLogger(root, chainio.Subsystem, interceptor, chainio.UseLogger)
|
AddSubLogger(root, chainio.Subsystem, interceptor, chainio.UseLogger)
|
||||||
|
AddSubLogger(root, msgmux.Subsystem, interceptor, msgmux.UseLogger)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddSubLogger is a helper method to conveniently create and register the
|
// AddSubLogger is a helper method to conveniently create and register the
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package msgmux
|
package msgmux
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -46,7 +47,7 @@ type Endpoint interface {
|
|||||||
|
|
||||||
// SendMessage handles the target message, and returns true if the
|
// SendMessage handles the target message, and returns true if the
|
||||||
// message was able being processed.
|
// message was able being processed.
|
||||||
SendMessage(msg PeerMsg) bool
|
SendMessage(ctx context.Context, msg PeerMsg) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// MsgRouter is an interface that represents a message router, which is generic
|
// MsgRouter is an interface that represents a message router, which is generic
|
||||||
@@ -66,7 +67,7 @@ type Router interface {
|
|||||||
RouteMsg(PeerMsg) error
|
RouteMsg(PeerMsg) error
|
||||||
|
|
||||||
// Start starts the peer message router.
|
// Start starts the peer message router.
|
||||||
Start()
|
Start(ctx context.Context)
|
||||||
|
|
||||||
// Stop stops the peer message router.
|
// Stop stops the peer message router.
|
||||||
Stop()
|
Stop()
|
||||||
@@ -137,12 +138,12 @@ func NewMultiMsgRouter() *MultiMsgRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start starts the peer message router.
|
// Start starts the peer message router.
|
||||||
func (p *MultiMsgRouter) Start() {
|
func (p *MultiMsgRouter) Start(ctx context.Context) {
|
||||||
log.Infof("Starting Router")
|
log.Infof("Starting Router")
|
||||||
|
|
||||||
p.startOnce.Do(func() {
|
p.startOnce.Do(func() {
|
||||||
p.wg.Add(1)
|
p.wg.Add(1)
|
||||||
go p.msgRouter()
|
go p.msgRouter(ctx)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,7 +180,7 @@ func (p *MultiMsgRouter) endpoints() fn.Result[EndpointsMap] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// msgRouter is the main goroutine that handles all incoming messages.
|
// msgRouter is the main goroutine that handles all incoming messages.
|
||||||
func (p *MultiMsgRouter) msgRouter() {
|
func (p *MultiMsgRouter) msgRouter(ctx context.Context) {
|
||||||
defer p.wg.Done()
|
defer p.wg.Done()
|
||||||
|
|
||||||
// endpoints is a map of all registered endpoints.
|
// endpoints is a map of all registered endpoints.
|
||||||
@@ -235,7 +236,7 @@ func (p *MultiMsgRouter) msgRouter() {
|
|||||||
"msg %T to endpoint %s", msg,
|
"msg %T to endpoint %s", msg,
|
||||||
endpoint.Name())
|
endpoint.Name())
|
||||||
|
|
||||||
sent := endpoint.SendMessage(msg)
|
sent := endpoint.SendMessage(ctx, msg)
|
||||||
couldSend = couldSend || sent
|
couldSend = couldSend || sent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -243,7 +244,7 @@ func (p *MultiMsgRouter) msgRouter() {
|
|||||||
var err error
|
var err error
|
||||||
if !couldSend {
|
if !couldSend {
|
||||||
log.Tracef("MsgRouter: unable to route "+
|
log.Tracef("MsgRouter: unable to route "+
|
||||||
"msg %T", msg)
|
"msg %T", msg.Message)
|
||||||
|
|
||||||
err = ErrUnableToRouteMsg
|
err = ErrUnableToRouteMsg
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package msgmux
|
package msgmux
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
@@ -24,8 +25,8 @@ func (m *mockEndpoint) CanHandle(msg PeerMsg) bool {
|
|||||||
return args.Bool(0)
|
return args.Bool(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockEndpoint) SendMessage(msg PeerMsg) bool {
|
func (m *mockEndpoint) SendMessage(ctx context.Context, msg PeerMsg) bool {
|
||||||
args := m.Called(msg)
|
args := m.Called(ctx, msg)
|
||||||
|
|
||||||
return args.Bool(0)
|
return args.Bool(0)
|
||||||
}
|
}
|
||||||
@@ -33,8 +34,9 @@ func (m *mockEndpoint) SendMessage(msg PeerMsg) bool {
|
|||||||
// TestMessageRouterOperation tests the basic operation of the message router:
|
// TestMessageRouterOperation tests the basic operation of the message router:
|
||||||
// add new endpoints, route to them, remove, them, etc.
|
// add new endpoints, route to them, remove, them, etc.
|
||||||
func TestMessageRouterOperation(t *testing.T) {
|
func TestMessageRouterOperation(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
msgRouter := NewMultiMsgRouter()
|
msgRouter := NewMultiMsgRouter()
|
||||||
msgRouter.Start()
|
msgRouter.Start(ctx)
|
||||||
defer msgRouter.Stop()
|
defer msgRouter.Stop()
|
||||||
|
|
||||||
openChanMsg := PeerMsg{
|
openChanMsg := PeerMsg{
|
||||||
@@ -57,7 +59,7 @@ func TestMessageRouterOperation(t *testing.T) {
|
|||||||
fundingEndpoint.On("CanHandle", openChanMsg).Return(true)
|
fundingEndpoint.On("CanHandle", openChanMsg).Return(true)
|
||||||
fundingEndpoint.On("CanHandle", errorMsg).Return(false)
|
fundingEndpoint.On("CanHandle", errorMsg).Return(false)
|
||||||
fundingEndpoint.On("CanHandle", commitSigMsg).Return(false)
|
fundingEndpoint.On("CanHandle", commitSigMsg).Return(false)
|
||||||
fundingEndpoint.On("SendMessage", openChanMsg).Return(true)
|
fundingEndpoint.On("SendMessage", ctx, openChanMsg).Return(true)
|
||||||
|
|
||||||
commitEndpoint := &mockEndpoint{}
|
commitEndpoint := &mockEndpoint{}
|
||||||
commitEndpointName := "commit"
|
commitEndpointName := "commit"
|
||||||
@@ -65,7 +67,7 @@ func TestMessageRouterOperation(t *testing.T) {
|
|||||||
commitEndpoint.On("CanHandle", commitSigMsg).Return(true)
|
commitEndpoint.On("CanHandle", commitSigMsg).Return(true)
|
||||||
commitEndpoint.On("CanHandle", openChanMsg).Return(false)
|
commitEndpoint.On("CanHandle", openChanMsg).Return(false)
|
||||||
commitEndpoint.On("CanHandle", errorMsg).Return(false)
|
commitEndpoint.On("CanHandle", errorMsg).Return(false)
|
||||||
commitEndpoint.On("SendMessage", commitSigMsg).Return(true)
|
commitEndpoint.On("SendMessage", ctx, commitSigMsg).Return(true)
|
||||||
|
|
||||||
t.Run("add endpoints", func(t *testing.T) {
|
t.Run("add endpoints", func(t *testing.T) {
|
||||||
// First, we'll add the funding endpoint to the router.
|
// First, we'll add the funding endpoint to the router.
|
||||||
@@ -113,8 +115,10 @@ func TestMessageRouterOperation(t *testing.T) {
|
|||||||
fundingEndpoint.AssertCalled(t, "CanHandle", openChanMsg)
|
fundingEndpoint.AssertCalled(t, "CanHandle", openChanMsg)
|
||||||
commitEndpoint.AssertCalled(t, "CanHandle", openChanMsg)
|
commitEndpoint.AssertCalled(t, "CanHandle", openChanMsg)
|
||||||
|
|
||||||
fundingEndpoint.AssertCalled(t, "SendMessage", openChanMsg)
|
fundingEndpoint.AssertCalled(t, "SendMessage", ctx, openChanMsg)
|
||||||
commitEndpoint.AssertNotCalled(t, "SendMessage", openChanMsg)
|
commitEndpoint.AssertNotCalled(
|
||||||
|
t, "SendMessage", ctx, openChanMsg,
|
||||||
|
)
|
||||||
|
|
||||||
// We'll do the same for the commit sig message.
|
// We'll do the same for the commit sig message.
|
||||||
require.NoError(t, msgRouter.RouteMsg(commitSigMsg))
|
require.NoError(t, msgRouter.RouteMsg(commitSigMsg))
|
||||||
@@ -122,8 +126,10 @@ func TestMessageRouterOperation(t *testing.T) {
|
|||||||
fundingEndpoint.AssertCalled(t, "CanHandle", commitSigMsg)
|
fundingEndpoint.AssertCalled(t, "CanHandle", commitSigMsg)
|
||||||
commitEndpoint.AssertCalled(t, "CanHandle", commitSigMsg)
|
commitEndpoint.AssertCalled(t, "CanHandle", commitSigMsg)
|
||||||
|
|
||||||
commitEndpoint.AssertCalled(t, "SendMessage", commitSigMsg)
|
commitEndpoint.AssertCalled(t, "SendMessage", ctx, commitSigMsg)
|
||||||
fundingEndpoint.AssertNotCalled(t, "SendMessage", commitSigMsg)
|
fundingEndpoint.AssertNotCalled(
|
||||||
|
t, "SendMessage", ctx, commitSigMsg,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("remove endpoints", func(t *testing.T) {
|
t.Run("remove endpoints", func(t *testing.T) {
|
||||||
|
|||||||
+1031
-161
File diff suppressed because it is too large.
Load diff
+20
-11
@@ -689,15 +689,9 @@ func TestChooseDeliveryScript(t *testing.T) {
|
|||||||
userScript lnwire.DeliveryAddress
|
userScript lnwire.DeliveryAddress
|
||||||
shutdownScript lnwire.DeliveryAddress
|
shutdownScript lnwire.DeliveryAddress
|
||||||
expectedScript lnwire.DeliveryAddress
|
expectedScript lnwire.DeliveryAddress
|
||||||
|
newAddr func() ([]byte, error)
|
||||||
expectedError error
|
expectedError error
|
||||||
}{
|
}{
|
||||||
{
|
|
||||||
name: "Neither set",
|
|
||||||
userScript: nil,
|
|
||||||
shutdownScript: nil,
|
|
||||||
expectedScript: nil,
|
|
||||||
expectedError: nil,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "Both set and equal",
|
name: "Both set and equal",
|
||||||
userScript: script1,
|
userScript: script1,
|
||||||
@@ -726,6 +720,16 @@ func TestChooseDeliveryScript(t *testing.T) {
|
|||||||
expectedScript: script2,
|
expectedScript: script2,
|
||||||
expectedError: nil,
|
expectedError: nil,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "no script generate new one",
|
||||||
|
userScript: nil,
|
||||||
|
shutdownScript: nil,
|
||||||
|
expectedScript: script2,
|
||||||
|
newAddr: func() ([]byte, error) {
|
||||||
|
return script2, nil
|
||||||
|
},
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
@@ -734,13 +738,16 @@ func TestChooseDeliveryScript(t *testing.T) {
|
|||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
script, err := chooseDeliveryScript(
|
script, err := chooseDeliveryScript(
|
||||||
test.shutdownScript, test.userScript,
|
test.shutdownScript, test.userScript,
|
||||||
|
test.newAddr,
|
||||||
)
|
)
|
||||||
if err != test.expectedError {
|
if err != test.expectedError {
|
||||||
t.Fatalf("Expected: %v, got: %v", test.expectedError, err)
|
t.Fatalf("Expected: %v, got: %v",
|
||||||
|
test.expectedError, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !bytes.Equal(script, test.expectedScript) {
|
if !bytes.Equal(script, test.expectedScript) {
|
||||||
t.Fatalf("Expected: %x, got: %x", test.expectedScript, script)
|
t.Fatalf("Expected: %x, got: %x",
|
||||||
|
test.expectedScript, script)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -853,8 +860,10 @@ func TestCustomShutdownScript(t *testing.T) {
|
|||||||
t.Fatalf("did not receive shutdown message")
|
t.Fatalf("did not receive shutdown message")
|
||||||
case err := <-errChan:
|
case err := <-errChan:
|
||||||
// Fail if we do not expect an error.
|
// Fail if we do not expect an error.
|
||||||
if err != test.expectedError {
|
if test.expectedError != nil {
|
||||||
t.Fatalf("error closing channel: %v", err)
|
require.ErrorIs(
|
||||||
|
t, err, test.expectedError,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Terminate the test early if have received an error, no
|
// Terminate the test early if have received an error, no
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
package peer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
|
"github.com/lightningnetwork/lnd/fn/v2"
|
||||||
|
"github.com/lightningnetwork/lnd/htlcswitch"
|
||||||
|
"github.com/lightningnetwork/lnd/lntypes"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwallet/chancloser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// channelView is a view into the current active/global channel state machine
|
||||||
|
// for a given link.
|
||||||
|
type channelView interface {
|
||||||
|
// OweCommitment returns a boolean value reflecting whether we need to
|
||||||
|
// send out a commitment signature because there are outstanding local
|
||||||
|
// updates and/or updates in the local commit tx that aren't reflected
|
||||||
|
// in the remote commit tx yet.
|
||||||
|
OweCommitment() bool
|
||||||
|
|
||||||
|
// IsChannelClean returns true if neither side has pending commitments,
|
||||||
|
// neither side has HTLC's, and all updates are locked in irrevocably.
|
||||||
|
IsChannelClean() bool
|
||||||
|
|
||||||
|
// MarkCoopBroadcasted persistently marks that the channel close
|
||||||
|
// transaction has been broadcast.
|
||||||
|
MarkCoopBroadcasted(*wire.MsgTx, lntypes.ChannelParty) error
|
||||||
|
|
||||||
|
// StateSnapshot returns a snapshot of the current fully committed
|
||||||
|
// state within the channel.
|
||||||
|
StateSnapshot() *channeldb.ChannelSnapshot
|
||||||
|
|
||||||
|
// MarkShutdownSent persists the given ShutdownInfo. The existence of
|
||||||
|
// the ShutdownInfo represents the fact that the Shutdown message has
|
||||||
|
// been sent by us and so should be re-sent on re-establish.
|
||||||
|
MarkShutdownSent(info *channeldb.ShutdownInfo) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// linkController is capable of controlling the flow out incoming/outgoing
|
||||||
|
// HTLCs to/from the link.
|
||||||
|
type linkController interface {
|
||||||
|
// DisableAdds sets the ChannelUpdateHandler state to allow/reject
|
||||||
|
// UpdateAddHtlc's in the specified direction. It returns true if the
|
||||||
|
// state was changed and false if the desired state was already set
|
||||||
|
// before the method was called.
|
||||||
|
DisableAdds(outgoing bool) bool
|
||||||
|
|
||||||
|
// IsFlushing returns true when UpdateAddHtlc's are disabled in the
|
||||||
|
// direction of the argument.
|
||||||
|
IsFlushing(direction bool) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// linkNetworkController is an interface that represents an object capable of
|
||||||
|
// managing interactions with the active channel links from the PoV of the
|
||||||
|
// gossip network.
|
||||||
|
type linkNetworkController interface {
|
||||||
|
// RequestDisable disables a channel by its channel point.
|
||||||
|
RequestDisable(wire.OutPoint, bool) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// chanObserver implements the chancloser.ChanObserver interface for the
|
||||||
|
// existing LightningChannel struct/instance.
|
||||||
|
type chanObserver struct {
|
||||||
|
chanView channelView
|
||||||
|
link linkController
|
||||||
|
linkNetwork linkNetworkController
|
||||||
|
}
|
||||||
|
|
||||||
|
// newChanObserver creates a new instance of a chanObserver from an active
|
||||||
|
// channelView.
|
||||||
|
func newChanObserver(chanView channelView,
|
||||||
|
link linkController, linkNetwork linkNetworkController) *chanObserver {
|
||||||
|
|
||||||
|
return &chanObserver{
|
||||||
|
chanView: chanView,
|
||||||
|
link: link,
|
||||||
|
linkNetwork: linkNetwork,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NoDanglingUpdates returns true if there are no dangling updates in the
|
||||||
|
// channel. In other words, there are no active update messages that haven't
|
||||||
|
// already been covered by a commit sig.
|
||||||
|
func (l *chanObserver) NoDanglingUpdates() bool {
|
||||||
|
return !l.chanView.OweCommitment()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisableIncomingAdds instructs the channel link to disable process new
|
||||||
|
// incoming add messages.
|
||||||
|
func (l *chanObserver) DisableIncomingAdds() error {
|
||||||
|
// If there's no link, then we don't need to disable any adds.
|
||||||
|
if l.link == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
disabled := l.link.DisableAdds(htlcswitch.Incoming)
|
||||||
|
if disabled {
|
||||||
|
chanPoint := l.chanView.StateSnapshot().ChannelPoint
|
||||||
|
peerLog.Debugf("ChannelPoint(%v): link already disabled",
|
||||||
|
chanPoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisableOutgoingAdds instructs the channel link to disable process new
|
||||||
|
// outgoing add messages.
|
||||||
|
func (l *chanObserver) DisableOutgoingAdds() error {
|
||||||
|
// If there's no link, then we don't need to disable any adds.
|
||||||
|
if l.link == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = l.link.DisableAdds(htlcswitch.Outgoing)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkCoopBroadcasted persistently marks that the channel close transaction
|
||||||
|
// has been broadcast.
|
||||||
|
func (l *chanObserver) MarkCoopBroadcasted(tx *wire.MsgTx, local bool) error {
|
||||||
|
return l.chanView.MarkCoopBroadcasted(tx, lntypes.Local)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkShutdownSent persists the given ShutdownInfo. The existence of the
|
||||||
|
// ShutdownInfo represents the fact that the Shutdown message has been sent by
|
||||||
|
// us and so should be re-sent on re-establish.
|
||||||
|
func (l *chanObserver) MarkShutdownSent(deliveryAddr []byte,
|
||||||
|
isInitiator bool) error {
|
||||||
|
|
||||||
|
shutdownInfo := channeldb.NewShutdownInfo(deliveryAddr, isInitiator)
|
||||||
|
return l.chanView.MarkShutdownSent(shutdownInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FinalBalances is the balances of the channel once it has been flushed. If
|
||||||
|
// Some, then this indicates that the channel is now in a state where it's
|
||||||
|
// always flushed, so we can accelerate the state transitions.
|
||||||
|
func (l *chanObserver) FinalBalances() fn.Option[chancloser.ShutdownBalances] {
|
||||||
|
chanClean := l.chanView.IsChannelClean()
|
||||||
|
|
||||||
|
switch {
|
||||||
|
// If we have a link, then the balances are final if both the incoming
|
||||||
|
// and outgoing adds are disabled _and_ the channel is clean.
|
||||||
|
case l.link != nil && l.link.IsFlushing(htlcswitch.Incoming) &&
|
||||||
|
l.link.IsFlushing(htlcswitch.Outgoing) && chanClean:
|
||||||
|
|
||||||
|
fallthrough
|
||||||
|
|
||||||
|
// If we don't have a link, then this is a restart case, so the
|
||||||
|
// balances are final.
|
||||||
|
case l.link == nil:
|
||||||
|
snapshot := l.chanView.StateSnapshot()
|
||||||
|
|
||||||
|
return fn.Some(chancloser.ShutdownBalances{
|
||||||
|
LocalBalance: snapshot.LocalBalance,
|
||||||
|
RemoteBalance: snapshot.RemoteBalance,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Otherwise, the link is still active and not flushed, so the balances
|
||||||
|
// aren't yet final.
|
||||||
|
default:
|
||||||
|
return fn.None[chancloser.ShutdownBalances]()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisableChannel disables the target channel.
|
||||||
|
func (l *chanObserver) DisableChannel() error {
|
||||||
|
op := l.chanView.StateSnapshot().ChannelPoint
|
||||||
|
return l.linkNetwork.RequestDisable(op, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A compile-time assertion to ensure that chanObserver meets the
|
||||||
|
// chancloser.ChanStateObserver interface.
|
||||||
|
var _ chancloser.ChanStateObserver = (*chanObserver)(nil)
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
package peer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
"github.com/lightningnetwork/lnd/protofsm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MessageSender is an interface that represents an object capable of sending
|
||||||
|
// p2p messages to a destination.
|
||||||
|
type MessageSender interface {
|
||||||
|
// SendMessages sends the target set of messages to the target peer.
|
||||||
|
//
|
||||||
|
// TODO(roasbeef): current impl bound to single peer, need server
|
||||||
|
// pointer otherwise
|
||||||
|
SendMessages(btcec.PublicKey, []lnwire.Message) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// flexMessageSender is a message sender-like interface that is aware of
|
||||||
|
// sync/async semantics, and is bound to a single peer.
|
||||||
|
type flexMessageSender interface {
|
||||||
|
// SendMessage sends a variadic number of high-priority messages to the
|
||||||
|
// remote peer. The first argument denotes if the method should block
|
||||||
|
// until the messages have been sent to the remote peer or an error is
|
||||||
|
// returned, otherwise it returns immediately after queuing.
|
||||||
|
SendMessage(sync bool, msgs ...lnwire.Message) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// peerMsgSender implements the MessageSender interface for a single peer.
|
||||||
|
// It'll return an error if the target public isn't equal to public key of the
|
||||||
|
// backing peer.
|
||||||
|
type peerMsgSender struct {
|
||||||
|
sender flexMessageSender
|
||||||
|
peerPub btcec.PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// newPeerMsgSender creates a new instance of a peerMsgSender.
|
||||||
|
func newPeerMsgSender(peerPub btcec.PublicKey,
|
||||||
|
msgSender flexMessageSender) *peerMsgSender {
|
||||||
|
|
||||||
|
return &peerMsgSender{
|
||||||
|
sender: msgSender,
|
||||||
|
peerPub: peerPub,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendMessages sends the target set of messages to the target peer.
|
||||||
|
//
|
||||||
|
// TODO(roasbeef): current impl bound to single peer, need server pointer
|
||||||
|
// otherwise?
|
||||||
|
func (p *peerMsgSender) SendMessages(pub btcec.PublicKey,
|
||||||
|
msgs []lnwire.Message) error {
|
||||||
|
|
||||||
|
if !p.peerPub.IsEqual(&pub) {
|
||||||
|
return fmt.Errorf("wrong peer pubkey: got %x, can only send "+
|
||||||
|
"to %x", pub.SerializeCompressed(),
|
||||||
|
p.peerPub.SerializeCompressed())
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.sender.SendMessage(true, msgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TxBroadcaster is an interface that represents an object capable of
|
||||||
|
// broadcasting transactions to the network.
|
||||||
|
type TxBroadcaster interface {
|
||||||
|
// PublishTransaction broadcasts a transaction to the network.
|
||||||
|
PublishTransaction(*wire.MsgTx, string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// LndAdapterCfg is a struct that holds the configuration for the
|
||||||
|
// LndDaemonAdapters instance.
|
||||||
|
type LndAdapterCfg struct {
|
||||||
|
// MsgSender is capable of sending messages to an arbitrary peer.
|
||||||
|
MsgSender MessageSender
|
||||||
|
|
||||||
|
// TxBroadcaster is capable of broadcasting a transaction to the
|
||||||
|
// network.
|
||||||
|
TxBroadcaster TxBroadcaster
|
||||||
|
|
||||||
|
// ChainNotifier is capable of receiving notifications for on-chain
|
||||||
|
// events.
|
||||||
|
ChainNotifier chainntnfs.ChainNotifier
|
||||||
|
}
|
||||||
|
|
||||||
|
// LndDaemonAdapters is a struct that implements the protofsm.DaemonAdapters
|
||||||
|
// interface using common lnd abstractions.
|
||||||
|
type LndDaemonAdapters struct {
|
||||||
|
cfg LndAdapterCfg
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLndDaemonAdapters creates a new instance of the lndDaemonAdapters struct.
|
||||||
|
func NewLndDaemonAdapters(cfg LndAdapterCfg) *LndDaemonAdapters {
|
||||||
|
return &LndDaemonAdapters{
|
||||||
|
cfg: cfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendMessages sends the target set of messages to the target peer.
|
||||||
|
func (l *LndDaemonAdapters) SendMessages(pub btcec.PublicKey,
|
||||||
|
msgs []lnwire.Message) error {
|
||||||
|
|
||||||
|
return l.cfg.MsgSender.SendMessages(pub, msgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BroadcastTransaction broadcasts a transaction with the target label.
|
||||||
|
func (l *LndDaemonAdapters) BroadcastTransaction(tx *wire.MsgTx,
|
||||||
|
label string) error {
|
||||||
|
|
||||||
|
return l.cfg.TxBroadcaster.PublishTransaction(tx, label)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterConfirmationsNtfn registers an intent to be notified once txid
|
||||||
|
// reaches numConfs confirmations.
|
||||||
|
func (l *LndDaemonAdapters) RegisterConfirmationsNtfn(txid *chainhash.Hash,
|
||||||
|
pkScript []byte, numConfs, heightHint uint32,
|
||||||
|
opts ...chainntnfs.NotifierOption,
|
||||||
|
) (*chainntnfs.ConfirmationEvent, error) {
|
||||||
|
|
||||||
|
return l.cfg.ChainNotifier.RegisterConfirmationsNtfn(
|
||||||
|
txid, pkScript, numConfs, heightHint, opts...,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterSpendNtfn registers an intent to be notified once the target
|
||||||
|
// outpoint is successfully spent within a transaction.
|
||||||
|
func (l *LndDaemonAdapters) RegisterSpendNtfn(outpoint *wire.OutPoint,
|
||||||
|
pkScript []byte, heightHint uint32) (*chainntnfs.SpendEvent, error) {
|
||||||
|
|
||||||
|
return l.cfg.ChainNotifier.RegisterSpendNtfn(
|
||||||
|
outpoint, pkScript, heightHint,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A compile time check to ensure that lndDaemonAdapters fully implements the
|
||||||
|
// DaemonAdapters interface.
|
||||||
|
var _ protofsm.DaemonAdapters = (*LndDaemonAdapters)(nil)
|
||||||
+1
-1
@@ -337,7 +337,7 @@ func createTestPeerWithChannel(t *testing.T, updateChan func(a,
|
|||||||
chanID := lnwire.NewChanIDFromOutPoint(channelAlice.ChannelPoint())
|
chanID := lnwire.NewChanIDFromOutPoint(channelAlice.ChannelPoint())
|
||||||
alicePeer.activeChannels.Store(chanID, channelAlice)
|
alicePeer.activeChannels.Store(chanID, channelAlice)
|
||||||
|
|
||||||
alicePeer.wg.Add(1)
|
alicePeer.cg.WgAdd(1)
|
||||||
go alicePeer.channelManager()
|
go alicePeer.channelManager()
|
||||||
|
|
||||||
return &peerTestCtx{
|
return &peerTestCtx{
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package protofsm
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/lightningnetwork/lnd/fn/v2"
|
"github.com/lightningnetwork/lnd/fn/v2"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/msgmux"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MsgMapper is used to map incoming wire messages into a FSM event. This is
|
// MsgMapper is used to map incoming wire messages into a FSM event. This is
|
||||||
@@ -11,5 +11,5 @@ import (
|
|||||||
type MsgMapper[Event any] interface {
|
type MsgMapper[Event any] interface {
|
||||||
// MapMsg maps a wire message into a FSM event. If the message is not
|
// MapMsg maps a wire message into a FSM event. If the message is not
|
||||||
// mappable, then an None is returned.
|
// mappable, then an None is returned.
|
||||||
MapMsg(msg lnwire.Message) fn.Option[Event]
|
MapMsg(msg msgmux.PeerMsg) fn.Option[Event]
|
||||||
}
|
}
|
||||||
+22
-18
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/fn/v2"
|
"github.com/lightningnetwork/lnd/fn/v2"
|
||||||
"github.com/lightningnetwork/lnd/lnutils"
|
"github.com/lightningnetwork/lnd/lnutils"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
"github.com/lightningnetwork/lnd/msgmux"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -77,7 +78,8 @@ type State[Event any, Env Environment] interface {
|
|||||||
// otherwise.
|
// otherwise.
|
||||||
IsTerminal() bool
|
IsTerminal() bool
|
||||||
|
|
||||||
// TODO(roasbeef): also add state serialization?
|
// String returns a human readable string that represents the state.
|
||||||
|
String() string
|
||||||
}
|
}
|
||||||
|
|
||||||
// DaemonAdapters is a set of methods that server as adapters to bridge the
|
// DaemonAdapters is a set of methods that server as adapters to bridge the
|
||||||
@@ -249,7 +251,7 @@ func (s *StateMachine[Event, Env]) SendEvent(ctx context.Context, event Event) {
|
|||||||
|
|
||||||
// CanHandle returns true if the target message can be routed to the state
|
// CanHandle returns true if the target message can be routed to the state
|
||||||
// machine.
|
// machine.
|
||||||
func (s *StateMachine[Event, Env]) CanHandle(msg lnwire.Message) bool {
|
func (s *StateMachine[Event, Env]) CanHandle(msg msgmux.PeerMsg) bool {
|
||||||
cfgMapper := s.cfg.MsgMapper
|
cfgMapper := s.cfg.MsgMapper
|
||||||
return fn.MapOptionZ(cfgMapper, func(mapper MsgMapper[Event]) bool {
|
return fn.MapOptionZ(cfgMapper, func(mapper MsgMapper[Event]) bool {
|
||||||
return mapper.MapMsg(msg).IsSome()
|
return mapper.MapMsg(msg).IsSome()
|
||||||
@@ -266,7 +268,7 @@ func (s *StateMachine[Event, Env]) Name() string {
|
|||||||
// returned indicating that the message was processed. Otherwise, false is
|
// returned indicating that the message was processed. Otherwise, false is
|
||||||
// returned.
|
// returned.
|
||||||
func (s *StateMachine[Event, Env]) SendMessage(ctx context.Context,
|
func (s *StateMachine[Event, Env]) SendMessage(ctx context.Context,
|
||||||
msg lnwire.Message) bool {
|
msg msgmux.PeerMsg) bool {
|
||||||
|
|
||||||
// If we have no message mapper, then return false as we can't process
|
// If we have no message mapper, then return false as we can't process
|
||||||
// this message.
|
// this message.
|
||||||
@@ -343,7 +345,7 @@ func (s *StateMachine[Event, Env]) executeDaemonEvent(ctx context.Context,
|
|||||||
// any preconditions as well as post-send events.
|
// any preconditions as well as post-send events.
|
||||||
case *SendMsgEvent[Event]:
|
case *SendMsgEvent[Event]:
|
||||||
sendAndCleanUp := func() error {
|
sendAndCleanUp := func() error {
|
||||||
s.log.DebugS(ctx, "Sending message to target",
|
s.log.DebugS(ctx, "Sending message:",
|
||||||
btclog.Hex6("target", daemonEvent.TargetPeer.SerializeCompressed()),
|
btclog.Hex6("target", daemonEvent.TargetPeer.SerializeCompressed()),
|
||||||
"messages", lnutils.SpewLogClosure(daemonEvent.Msgs))
|
"messages", lnutils.SpewLogClosure(daemonEvent.Msgs))
|
||||||
|
|
||||||
@@ -375,9 +377,18 @@ func (s *StateMachine[Event, Env]) executeDaemonEvent(ctx context.Context,
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this doesn't have a SendWhen predicate, then we can just
|
canSend := func() bool {
|
||||||
// send it off right away.
|
return fn.MapOptionZ(
|
||||||
if !daemonEvent.SendWhen.IsSome() {
|
daemonEvent.SendWhen,
|
||||||
|
func(pred SendPredicate) bool {
|
||||||
|
return pred()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this doesn't have a SendWhen predicate, or if it's already
|
||||||
|
// true, then we can just send it off right away.
|
||||||
|
if !daemonEvent.SendWhen.IsSome() || canSend() {
|
||||||
return sendAndCleanUp()
|
return sendAndCleanUp()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,14 +406,7 @@ func (s *StateMachine[Event, Env]) executeDaemonEvent(ctx context.Context,
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-predicateTicker.C:
|
case <-predicateTicker.C:
|
||||||
canSend := fn.MapOptionZ(
|
if canSend() {
|
||||||
daemonEvent.SendWhen,
|
|
||||||
func(pred SendPredicate) bool {
|
|
||||||
return pred()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if canSend {
|
|
||||||
s.log.InfoS(ctx, "Send active predicate")
|
s.log.InfoS(ctx, "Send active predicate")
|
||||||
|
|
||||||
err := sendAndCleanUp()
|
err := sendAndCleanUp()
|
||||||
@@ -435,7 +439,7 @@ func (s *StateMachine[Event, Env]) executeDaemonEvent(ctx context.Context,
|
|||||||
daemonEvent.Tx, daemonEvent.Label,
|
daemonEvent.Tx, daemonEvent.Label,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to broadcast txn: %w", err)
|
log.Errorf("unable to broadcast txn: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -597,8 +601,8 @@ func (s *StateMachine[Event, Env]) applyEvents(ctx context.Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
s.log.InfoS(ctx, "State transition",
|
s.log.InfoS(ctx, "State transition",
|
||||||
btclog.Fmt("from_state", "%T", currentState),
|
btclog.Fmt("from_state", "%v", currentState),
|
||||||
btclog.Fmt("to_state", "%T", transition.NextState))
|
btclog.Fmt("to_state", "%v", transition.NextState))
|
||||||
|
|
||||||
// With our events processed, we'll now update our
|
// With our events processed, we'll now update our
|
||||||
// internal state.
|
// internal state.
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||||
"github.com/lightningnetwork/lnd/fn/v2"
|
"github.com/lightningnetwork/lnd/fn/v2"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
"github.com/lightningnetwork/lnd/msgmux"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -51,6 +52,10 @@ type dummyStateStart struct {
|
|||||||
canSend *atomic.Bool
|
canSend *atomic.Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *dummyStateStart) String() string {
|
||||||
|
return "dummyStateStart"
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
hexDecode = func(keyStr string) []byte {
|
hexDecode = func(keyStr string) []byte {
|
||||||
keyBytes, _ := hex.DecodeString(keyStr)
|
keyBytes, _ := hex.DecodeString(keyStr)
|
||||||
@@ -134,6 +139,10 @@ func (d *dummyStateStart) IsTerminal() bool {
|
|||||||
type dummyStateFin struct {
|
type dummyStateFin struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *dummyStateFin) String() string {
|
||||||
|
return "dummyStateFin"
|
||||||
|
}
|
||||||
|
|
||||||
func (d *dummyStateFin) ProcessEvent(event dummyEvents, env *dummyEnv,
|
func (d *dummyStateFin) ProcessEvent(event dummyEvents, env *dummyEnv,
|
||||||
) (*StateTransition[dummyEvents, *dummyEnv], error) {
|
) (*StateTransition[dummyEvents, *dummyEnv], error) {
|
||||||
|
|
||||||
@@ -397,7 +406,7 @@ type dummyMsgMapper struct {
|
|||||||
mock.Mock
|
mock.Mock
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *dummyMsgMapper) MapMsg(wireMsg lnwire.Message) fn.Option[dummyEvents] {
|
func (d *dummyMsgMapper) MapMsg(wireMsg msgmux.PeerMsg) fn.Option[dummyEvents] {
|
||||||
args := d.Called(wireMsg)
|
args := d.Called(wireMsg)
|
||||||
|
|
||||||
//nolint:forcetypeassert
|
//nolint:forcetypeassert
|
||||||
@@ -421,8 +430,12 @@ func TestStateMachineMsgMapper(t *testing.T) {
|
|||||||
|
|
||||||
// The only thing we know how to map is the error message, which'll
|
// The only thing we know how to map is the error message, which'll
|
||||||
// terminate the state machine.
|
// terminate the state machine.
|
||||||
wireError := &lnwire.Error{}
|
wireError := msgmux.PeerMsg{
|
||||||
initMsg := &lnwire.Init{}
|
Message: &lnwire.Error{},
|
||||||
|
}
|
||||||
|
initMsg := msgmux.PeerMsg{
|
||||||
|
Message: &lnwire.Init{},
|
||||||
|
}
|
||||||
dummyMapper.On("MapMsg", wireError).Return(
|
dummyMapper.On("MapMsg", wireError).Return(
|
||||||
fn.Some(dummyEvents(&goToFin{})),
|
fn.Some(dummyEvents(&goToFin{})),
|
||||||
)
|
)
|
||||||
@@ -448,7 +461,7 @@ func TestStateMachineMsgMapper(t *testing.T) {
|
|||||||
|
|
||||||
// First, we'll verify that the CanHandle method works as expected.
|
// First, we'll verify that the CanHandle method works as expected.
|
||||||
require.True(t, stateMachine.CanHandle(wireError))
|
require.True(t, stateMachine.CanHandle(wireError))
|
||||||
require.False(t, stateMachine.CanHandle(&lnwire.Init{}))
|
require.False(t, stateMachine.CanHandle(initMsg))
|
||||||
|
|
||||||
// Next, we'll attempt to send the wire message into the state machine.
|
// Next, we'll attempt to send the wire message into the state machine.
|
||||||
// We should transition to the final state.
|
// We should transition to the final state.
|
||||||
|
|||||||
+64
-11
@@ -2801,14 +2801,33 @@ func (r *rpcServer) CloseChannel(in *lnrpc.CloseChannelRequest,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
chanInSwitch = true
|
||||||
|
chanHasRbfCloser = r.server.ChanHasRbfCoopCloser(
|
||||||
|
channel.IdentityPub, *chanPoint,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
// If the link is not known by the switch, we cannot gracefully close
|
// If the link is not known by the switch, we cannot gracefully close
|
||||||
// the channel.
|
// the channel.
|
||||||
channelID := lnwire.NewChanIDFromOutPoint(*chanPoint)
|
channelID := lnwire.NewChanIDFromOutPoint(*chanPoint)
|
||||||
|
|
||||||
if _, err := r.server.htlcSwitch.GetLink(channelID); err != nil {
|
if _, err := r.server.htlcSwitch.GetLink(channelID); err != nil {
|
||||||
rpcsLog.Debugf("Trying to non-force close offline channel with "+
|
chanInSwitch = false
|
||||||
"chan_point=%v", chanPoint)
|
|
||||||
return fmt.Errorf("unable to gracefully close channel while peer "+
|
// The channel isn't in the switch, but if there's an
|
||||||
"is offline (try force closing it instead): %v", err)
|
// active chan closer for the channel, and it's of the
|
||||||
|
// RBF variant, then we can actually bypass the switch.
|
||||||
|
// Otherwise, we'll return an error.
|
||||||
|
if !chanHasRbfCloser {
|
||||||
|
rpcsLog.Debugf("Trying to non-force close "+
|
||||||
|
"offline channel with chan_point=%v",
|
||||||
|
chanPoint)
|
||||||
|
|
||||||
|
return fmt.Errorf("unable to gracefully close "+
|
||||||
|
"channel while peer is offline (try "+
|
||||||
|
"force closing it instead): %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep the old behavior prior to 0.18.0 - when the user
|
// Keep the old behavior prior to 0.18.0 - when the user
|
||||||
@@ -2886,10 +2905,31 @@ func (r *rpcServer) CloseChannel(in *lnrpc.CloseChannelRequest,
|
|||||||
feeRate)
|
feeRate)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateChan, errChan = r.server.htlcSwitch.CloseLink(
|
if chanHasRbfCloser && !chanInSwitch {
|
||||||
chanPoint, contractcourt.CloseRegular, feeRate,
|
rpcsLog.Infof("Bypassing Switch to do fee bump "+
|
||||||
maxFee, deliveryScript,
|
"for ChannelPoint(%v)", chanPoint)
|
||||||
|
|
||||||
|
closeUpdates, err := r.server.AttemptRBFCloseUpdate(
|
||||||
|
updateStream.Context(), *chanPoint, feeRate,
|
||||||
|
deliveryScript,
|
||||||
)
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to do RBF close "+
|
||||||
|
"update: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateChan = closeUpdates.UpdateChan
|
||||||
|
errChan = closeUpdates.ErrChan
|
||||||
|
} else {
|
||||||
|
maxFee := chainfee.SatPerKVByte(
|
||||||
|
in.MaxFeePerVbyte * 1000,
|
||||||
|
).FeePerKWeight()
|
||||||
|
updateChan, errChan = r.server.htlcSwitch.CloseLink(
|
||||||
|
updateStream.Context(), chanPoint,
|
||||||
|
contractcourt.CloseRegular, feeRate, maxFee,
|
||||||
|
deliveryScript,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the user doesn't want to wait for the txid to come back then we
|
// If the user doesn't want to wait for the txid to come back then we
|
||||||
@@ -2911,6 +2951,7 @@ func (r *rpcServer) CloseChannel(in *lnrpc.CloseChannelRequest,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
out:
|
out:
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -2956,6 +2997,7 @@ out:
|
|||||||
h, _ := chainhash.NewHash(closeUpdate.ClosingTxid)
|
h, _ := chainhash.NewHash(closeUpdate.ClosingTxid)
|
||||||
rpcsLog.Infof("[closechannel] close completed: "+
|
rpcsLog.Infof("[closechannel] close completed: "+
|
||||||
"txid(%v)", h)
|
"txid(%v)", h)
|
||||||
|
|
||||||
break out
|
break out
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3046,12 +3088,23 @@ func createRPCCloseUpdate(
|
|||||||
}, nil
|
}, nil
|
||||||
|
|
||||||
case *peer.PendingUpdate:
|
case *peer.PendingUpdate:
|
||||||
return &lnrpc.CloseStatusUpdate{
|
upd := &lnrpc.PendingUpdate{
|
||||||
Update: &lnrpc.CloseStatusUpdate_ClosePending{
|
|
||||||
ClosePending: &lnrpc.PendingUpdate{
|
|
||||||
Txid: u.Txid,
|
Txid: u.Txid,
|
||||||
OutputIndex: u.OutputIndex,
|
OutputIndex: u.OutputIndex,
|
||||||
},
|
}
|
||||||
|
|
||||||
|
// Potentially set the optional fields that are only set for
|
||||||
|
// the new RBF close flow.
|
||||||
|
u.IsLocalCloseTx.WhenSome(func(isLocal bool) {
|
||||||
|
upd.LocalCloseTx = isLocal
|
||||||
|
})
|
||||||
|
u.FeePerVbyte.WhenSome(func(feeRate chainfee.SatPerVByte) {
|
||||||
|
upd.FeePerVbyte = int64(feeRate)
|
||||||
|
})
|
||||||
|
|
||||||
|
return &lnrpc.CloseStatusUpdate{
|
||||||
|
Update: &lnrpc.CloseStatusUpdate_ClosePending{
|
||||||
|
ClosePending: upd,
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1411,6 +1411,9 @@
|
|||||||
; Set to disable experimental endorsement signaling.
|
; Set to disable experimental endorsement signaling.
|
||||||
; protocol.no-experimental-endorsement=false
|
; protocol.no-experimental-endorsement=false
|
||||||
|
|
||||||
|
; Set to disable support for RBF based coop close.
|
||||||
|
; protocol.rbf-coop-close=false
|
||||||
|
|
||||||
; Set to handle messages of a particular type that falls outside of the
|
; Set to handle messages of a particular type that falls outside of the
|
||||||
; custom message number range (i.e. 513 is onion messages). Note that you can
|
; custom message number range (i.e. 513 is onion messages). Note that you can
|
||||||
; set this option as many times as you want to support more than one custom
|
; set this option as many times as you want to support more than one custom
|
||||||
|
|||||||
@@ -612,6 +612,17 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
|
|||||||
"aux controllers")
|
"aux controllers")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For now, the RBF coop close flag and the taproot channel type cannot
|
||||||
|
// be used together.
|
||||||
|
//
|
||||||
|
// TODO(roasbeef): fix
|
||||||
|
if cfg.ProtocolOptions.RbfCoopClose &&
|
||||||
|
cfg.ProtocolOptions.TaprootChans {
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("RBF coop close and taproot " +
|
||||||
|
"channels cannot be used together")
|
||||||
|
}
|
||||||
|
|
||||||
//nolint:ll
|
//nolint:ll
|
||||||
featureMgr, err := feature.NewManager(feature.Config{
|
featureMgr, err := feature.NewManager(feature.Config{
|
||||||
NoTLVOnion: cfg.ProtocolOptions.LegacyOnion(),
|
NoTLVOnion: cfg.ProtocolOptions.LegacyOnion(),
|
||||||
@@ -629,6 +640,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
|
|||||||
NoRouteBlinding: cfg.ProtocolOptions.NoRouteBlinding(),
|
NoRouteBlinding: cfg.ProtocolOptions.NoRouteBlinding(),
|
||||||
NoExperimentalEndorsement: cfg.ProtocolOptions.NoExperimentalEndorsement(),
|
NoExperimentalEndorsement: cfg.ProtocolOptions.NoExperimentalEndorsement(),
|
||||||
NoQuiescence: cfg.ProtocolOptions.NoQuiescence(),
|
NoQuiescence: cfg.ProtocolOptions.NoQuiescence(),
|
||||||
|
NoRbfCoopClose: !cfg.ProtocolOptions.RbfCoopClose,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1292,7 +1304,9 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
|
|||||||
// Instruct the switch to close the channel. Provide no close out
|
// Instruct the switch to close the channel. Provide no close out
|
||||||
// delivery script or target fee per kw because user input is not
|
// delivery script or target fee per kw because user input is not
|
||||||
// available when the remote peer closes the channel.
|
// available when the remote peer closes the channel.
|
||||||
s.htlcSwitch.CloseLink(chanPoint, closureType, 0, 0, nil)
|
s.htlcSwitch.CloseLink(
|
||||||
|
context.Background(), chanPoint, closureType, 0, 0, nil,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// We will use the following channel to reliably hand off contract
|
// We will use the following channel to reliably hand off contract
|
||||||
@@ -5413,3 +5427,88 @@ func (s *server) getStartingBeat() (*chainio.Beat, error) {
|
|||||||
|
|
||||||
return beat, nil
|
return beat, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ChanHasRbfCoopCloser returns true if the channel as identifier by the channel
|
||||||
|
// point has an active RBF chan closer.
|
||||||
|
func (s *server) ChanHasRbfCoopCloser(peerPub *btcec.PublicKey,
|
||||||
|
chanPoint wire.OutPoint) bool {
|
||||||
|
|
||||||
|
pubBytes := peerPub.SerializeCompressed()
|
||||||
|
|
||||||
|
s.mu.RLock()
|
||||||
|
targetPeer, ok := s.peersByPub[string(pubBytes)]
|
||||||
|
s.mu.RUnlock()
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetPeer.ChanHasRbfCoopCloser(chanPoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// attemptCoopRbfFeeBump attempts to look up the active chan closer for a
|
||||||
|
// channel given the outpoint. If found, we'll attempt to do a fee bump,
|
||||||
|
// returning channels used for updates. If the channel isn't currently active
|
||||||
|
// (p2p connection established), then his function will return an error.
|
||||||
|
func (s *server) attemptCoopRbfFeeBump(ctx context.Context,
|
||||||
|
chanPoint wire.OutPoint, feeRate chainfee.SatPerKWeight,
|
||||||
|
deliveryScript lnwire.DeliveryAddress) (*peer.CoopCloseUpdates, error) {
|
||||||
|
|
||||||
|
// First, we'll attempt to look up the channel based on it's
|
||||||
|
// ChannelPoint.
|
||||||
|
channel, err := s.chanStateDB.FetchChannel(chanPoint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to fetch channel: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// From the channel, we can now get the pubkey of the peer, then use
|
||||||
|
// that to eventually get the chan closer.
|
||||||
|
peerPub := channel.IdentityPub.SerializeCompressed()
|
||||||
|
|
||||||
|
// Now that we have the peer pub, we can look up the peer itself.
|
||||||
|
s.mu.RLock()
|
||||||
|
targetPeer, ok := s.peersByPub[string(peerPub)]
|
||||||
|
s.mu.RUnlock()
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("peer for ChannelPoint(%v) is "+
|
||||||
|
"not online", chanPoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
closeUpdates, err := targetPeer.TriggerCoopCloseRbfBump(
|
||||||
|
ctx, chanPoint, feeRate, deliveryScript,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to trigger coop rbf fee bump: "+
|
||||||
|
"%w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return closeUpdates, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AttemptRBFCloseUpdate attempts to trigger a new RBF iteration for a co-op
|
||||||
|
// close update. This route it to be used only if the target channel in question
|
||||||
|
// is no longer active in the link. This can happen when we restart while we
|
||||||
|
// already have done a single RBF co-op close iteration.
|
||||||
|
func (s *server) AttemptRBFCloseUpdate(ctx context.Context,
|
||||||
|
chanPoint wire.OutPoint, feeRate chainfee.SatPerKWeight,
|
||||||
|
deliveryScript lnwire.DeliveryAddress) (*peer.CoopCloseUpdates, error) {
|
||||||
|
|
||||||
|
// If the channel is present in the switch, then the request should flow
|
||||||
|
// through the switch instead.
|
||||||
|
chanID := lnwire.NewChanIDFromOutPoint(chanPoint)
|
||||||
|
if _, err := s.htlcSwitch.GetLink(chanID); err == nil {
|
||||||
|
return nil, fmt.Errorf("ChannelPoint(%v) is active in link, "+
|
||||||
|
"invalid request", chanPoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point, we know that the channel isn't present in the link, so
|
||||||
|
// we'll check to see if we have an entry in the active chan closer map.
|
||||||
|
updates, err := s.attemptCoopRbfFeeBump(
|
||||||
|
ctx, chanPoint, feeRate, deliveryScript,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to attempt coop rbf fee bump "+
|
||||||
|
"ChannelPoint(%v)", chanPoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
return updates, nil
|
||||||
|
}
|
||||||
Reference in new issue
Block a user