mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-02-22 22:25:24 +01:00
Merge pull request #4560 from guggero/abandon-shim-chans
Shim funded channels: don't count towards max pending channels, allow to abandon them
This commit is contained in:
commit
e4764a67cc
7 changed files with 954 additions and 851 deletions
|
@ -1210,9 +1210,21 @@ func (f *fundingManager) handleFundingOpen(fmsg *fundingOpenMsg) {
|
|||
// sum of the active reservations and the channels pending open in the
|
||||
// database.
|
||||
f.resMtx.RLock()
|
||||
numPending := len(f.activeReservations[peerIDKey])
|
||||
reservations := f.activeReservations[peerIDKey]
|
||||
f.resMtx.RUnlock()
|
||||
|
||||
// We don't count reservations that were created from a canned funding
|
||||
// shim. The user has registered the shim and therefore expects this
|
||||
// channel to arrive.
|
||||
numPending := 0
|
||||
for _, res := range reservations {
|
||||
if !res.reservation.IsCannedShim() {
|
||||
numPending++
|
||||
}
|
||||
}
|
||||
|
||||
// Also count the channels that are already pending. There we don't know
|
||||
// the underlying intent anymore, unfortunately.
|
||||
channels, err := f.cfg.Wallet.Cfg.Database.FetchOpenChannels(peerPubKey)
|
||||
if err != nil {
|
||||
f.failFundingFlow(
|
||||
|
@ -1222,7 +1234,13 @@ func (f *fundingManager) handleFundingOpen(fmsg *fundingOpenMsg) {
|
|||
}
|
||||
|
||||
for _, c := range channels {
|
||||
if c.IsPending {
|
||||
// Pending channels that have a non-zero thaw height were also
|
||||
// created through a canned funding shim. Those also don't
|
||||
// count towards the DoS protection limit.
|
||||
//
|
||||
// TODO(guggero): Properly store the funding type (wallet, shim,
|
||||
// PSBT) on the channel so we don't need to use the thaw height.
|
||||
if c.IsPending && c.ThawHeight == 0 {
|
||||
numPending++
|
||||
}
|
||||
}
|
||||
|
|
1503
lnrpc/rpc.pb.go
1503
lnrpc/rpc.pb.go
File diff suppressed because it is too large
Load diff
|
@ -235,8 +235,10 @@ service Lightning {
|
|||
/* lncli: `abandonchannel`
|
||||
AbandonChannel removes all channel state from the database except for a
|
||||
close summary. This method can be used to get rid of permanently unusable
|
||||
channels due to bugs fixed in newer versions of lnd. Only available
|
||||
when in debug builds of lnd.
|
||||
channels due to bugs fixed in newer versions of lnd. This method can also be
|
||||
used to remove externally funded channels where the funding transaction was
|
||||
never broadcast. Only available for non-externally funded channels in dev
|
||||
build.
|
||||
*/
|
||||
rpc AbandonChannel (AbandonChannelRequest) returns (AbandonChannelResponse);
|
||||
|
||||
|
@ -3058,6 +3060,8 @@ message DeleteAllPaymentsResponse {
|
|||
|
||||
message AbandonChannelRequest {
|
||||
ChannelPoint channel_point = 1;
|
||||
|
||||
bool pending_funding_shim_only = 2;
|
||||
}
|
||||
|
||||
message AbandonChannelResponse {
|
||||
|
|
|
@ -151,7 +151,7 @@
|
|||
},
|
||||
"/v1/channels/abandon/{channel_point.funding_txid_str}/{channel_point.output_index}": {
|
||||
"delete": {
|
||||
"summary": "lncli: `abandonchannel`\nAbandonChannel removes all channel state from the database except for a\nclose summary. This method can be used to get rid of permanently unusable\nchannels due to bugs fixed in newer versions of lnd. Only available\nwhen in debug builds of lnd.",
|
||||
"summary": "lncli: `abandonchannel`\nAbandonChannel removes all channel state from the database except for a\nclose summary. This method can be used to get rid of permanently unusable\nchannels due to bugs fixed in newer versions of lnd. This method can also be\nused to remove externally funded channels where the funding transaction was\nnever broadcast. Only available for non-externally funded channels in dev\nbuild.",
|
||||
"operationId": "AbandonChannel",
|
||||
"responses": {
|
||||
"200": {
|
||||
|
@ -190,6 +190,13 @@
|
|||
"required": false,
|
||||
"type": "string",
|
||||
"format": "byte"
|
||||
},
|
||||
{
|
||||
"name": "pending_funding_shim_only",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"type": "boolean",
|
||||
"format": "boolean"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
|
|
|
@ -13795,77 +13795,178 @@ func testExternalFundingChanPoint(net *lntest.NetworkHarness, t *harnessTest) {
|
|||
// First, we'll create two new nodes that we'll use to open channel
|
||||
// between for this test.
|
||||
carol, err := net.NewNode("carol", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to start new node: %v", err)
|
||||
}
|
||||
require.NoError(t.t, err)
|
||||
defer shutdownAndAssert(net, t, carol)
|
||||
|
||||
dave, err := net.NewNode("dave", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to start new node: %v", err)
|
||||
}
|
||||
require.NoError(t.t, err)
|
||||
defer shutdownAndAssert(net, t, dave)
|
||||
|
||||
// Carol will be funding the channel, so we'll send some coins over to
|
||||
// her and ensure they have enough confirmations before we proceed.
|
||||
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
||||
err = net.SendCoins(ctxt, btcutil.SatoshiPerBitcoin, carol)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to send coins to carol: %v", err)
|
||||
}
|
||||
require.NoError(t.t, err)
|
||||
|
||||
// Before we start the test, we'll ensure both sides are connected to
|
||||
// the funding flow can properly be executed.
|
||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||
err = net.EnsureConnected(ctxt, carol, dave)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to connect peers: %v", err)
|
||||
require.NoError(t.t, err)
|
||||
|
||||
// At this point, we're ready to simulate our external channel funding
|
||||
// flow. To start with, we'll create a pending channel with a shim for
|
||||
// a transaction that will never be published.
|
||||
const thawHeight uint32 = 10
|
||||
const chanSize = lnd.MaxBtcFundingAmount
|
||||
fundingShim1, chanPoint1, _ := deriveFundingShim(
|
||||
net, t, carol, dave, chanSize, thawHeight, 1, false,
|
||||
)
|
||||
_ = openChannelStream(
|
||||
ctxb, t, net, carol, dave, lntest.OpenChannelParams{
|
||||
Amt: chanSize,
|
||||
FundingShim: fundingShim1,
|
||||
},
|
||||
)
|
||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||
assertNumOpenChannelsPending(ctxt, t, carol, dave, 1)
|
||||
|
||||
// That channel is now pending forever and normally would saturate the
|
||||
// max pending channel limit for both nodes. But because the channel is
|
||||
// externally funded, we should still be able to open another one. Let's
|
||||
// do exactly that now. For this one we publish the transaction so we
|
||||
// can mine it later.
|
||||
fundingShim2, chanPoint2, _ := deriveFundingShim(
|
||||
net, t, carol, dave, chanSize, thawHeight, 2, true,
|
||||
)
|
||||
|
||||
// At this point, we'll now carry out the normal basic channel funding
|
||||
// test as everything should now proceed as normal (a regular channel
|
||||
// funding flow).
|
||||
carolChan, daveChan, _, err := basicChannelFundingTest(
|
||||
t, net, carol, dave, fundingShim2,
|
||||
)
|
||||
require.NoError(t.t, err)
|
||||
|
||||
// Both channels should be marked as frozen with the proper thaw
|
||||
// height.
|
||||
if carolChan.ThawHeight != thawHeight {
|
||||
t.Fatalf("expected thaw height of %v, got %v",
|
||||
carolChan.ThawHeight, thawHeight)
|
||||
}
|
||||
if daveChan.ThawHeight != thawHeight {
|
||||
t.Fatalf("expected thaw height of %v, got %v",
|
||||
daveChan.ThawHeight, thawHeight)
|
||||
}
|
||||
|
||||
// At this point, we're ready to simulate our external channle funding
|
||||
// flow. To start with, we'll get to new keys from both sides which
|
||||
// will be used to create the multi-sig output for the external funding
|
||||
// transaction.
|
||||
// Next, to make sure the channel functions as normal, we'll make some
|
||||
// payments within the channel.
|
||||
payAmt := btcutil.Amount(100000)
|
||||
invoice := &lnrpc.Invoice{
|
||||
Memo: "new chans",
|
||||
Value: int64(payAmt),
|
||||
}
|
||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||
resp, err := dave.AddInvoice(ctxt, invoice)
|
||||
require.NoError(t.t, err)
|
||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||
err = completePaymentRequests(
|
||||
ctxt, carol, carol.RouterClient, []string{resp.PaymentRequest},
|
||||
true,
|
||||
)
|
||||
require.NoError(t.t, err)
|
||||
|
||||
// Now that the channels are open, and we've confirmed that they're
|
||||
// operational, we'll now ensure that the channels are frozen as
|
||||
// intended (if requested).
|
||||
//
|
||||
// First, we'll try to close the channel as Carol, the initiator. This
|
||||
// should fail as a frozen channel only allows the responder to
|
||||
// initiate a channel close.
|
||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||
_, _, err = net.CloseChannel(ctxt, carol, chanPoint2, false)
|
||||
if err == nil {
|
||||
t.Fatalf("carol wasn't denied a co-op close attempt for a " +
|
||||
"frozen channel")
|
||||
}
|
||||
|
||||
// Next we'll try but this time with Dave (the responder) as the
|
||||
// initiator. This time the channel should be closed as normal.
|
||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||
closeChannelAndAssert(ctxt, t, net, dave, chanPoint2, false)
|
||||
|
||||
// As a last step, we check if we still have the pending channel hanging
|
||||
// around because we never published the funding TX.
|
||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||
assertNumOpenChannelsPending(ctxt, t, carol, dave, 1)
|
||||
|
||||
// Let's make sure we can abandon it.
|
||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||
_, err = carol.AbandonChannel(ctxt, &lnrpc.AbandonChannelRequest{
|
||||
ChannelPoint: chanPoint1,
|
||||
PendingFundingShimOnly: true,
|
||||
})
|
||||
require.NoError(t.t, err)
|
||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||
_, err = dave.AbandonChannel(ctxt, &lnrpc.AbandonChannelRequest{
|
||||
ChannelPoint: chanPoint1,
|
||||
PendingFundingShimOnly: true,
|
||||
})
|
||||
require.NoError(t.t, err)
|
||||
|
||||
// It should now not appear in the pending channels anymore.
|
||||
assertNumOpenChannelsPending(ctxt, t, carol, dave, 0)
|
||||
}
|
||||
|
||||
// deriveFundingShim creates a channel funding shim by deriving the necessary
|
||||
// keys on both sides.
|
||||
func deriveFundingShim(net *lntest.NetworkHarness, t *harnessTest,
|
||||
carol, dave *lntest.HarnessNode, chanSize btcutil.Amount,
|
||||
thawHeight uint32, keyIndex int32, publish bool) (*lnrpc.FundingShim,
|
||||
*lnrpc.ChannelPoint, *chainhash.Hash) {
|
||||
|
||||
ctxb := context.Background()
|
||||
keyLoc := &signrpc.KeyLocator{
|
||||
KeyFamily: 9999,
|
||||
KeyIndex: 1,
|
||||
KeyIndex: keyIndex,
|
||||
}
|
||||
carolFundingKey, err := carol.WalletKitClient.DeriveKey(ctxb, keyLoc)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to get carol funding key: %v", err)
|
||||
}
|
||||
require.NoError(t.t, err)
|
||||
daveFundingKey, err := dave.WalletKitClient.DeriveKey(ctxb, keyLoc)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to get dave funding key: %v", err)
|
||||
}
|
||||
require.NoError(t.t, err)
|
||||
|
||||
// Now that we have the multi-sig keys for each party, we can manually
|
||||
// construct the funding transaction. We'll instruct the backend to
|
||||
// immediately create and broadcast a transaction paying out an exact
|
||||
// amount. Normally this would reside in the mempool, but we just
|
||||
// confirm it now for simplicity.
|
||||
const chanSize = lnd.MaxBtcFundingAmount
|
||||
_, fundingOutput, err := input.GenFundingPkScript(
|
||||
carolFundingKey.RawKeyBytes, daveFundingKey.RawKeyBytes,
|
||||
int64(chanSize),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create funding script: %v", err)
|
||||
}
|
||||
txid, err := net.Miner.SendOutputsWithoutChange(
|
||||
[]*wire.TxOut{fundingOutput}, 5,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create funding output: %v", err)
|
||||
require.NoError(t.t, err)
|
||||
|
||||
var txid *chainhash.Hash
|
||||
targetOutputs := []*wire.TxOut{fundingOutput}
|
||||
if publish {
|
||||
txid, err = net.Miner.SendOutputsWithoutChange(
|
||||
targetOutputs, 5,
|
||||
)
|
||||
require.NoError(t.t, err)
|
||||
} else {
|
||||
tx, err := net.Miner.CreateTransaction(targetOutputs, 5, false)
|
||||
require.NoError(t.t, err)
|
||||
|
||||
txHash := tx.TxHash()
|
||||
txid = &txHash
|
||||
}
|
||||
|
||||
// At this point, we can being our external channel funding workflow.
|
||||
// We'll start by generating a pending channel ID externally that will
|
||||
// be used to track this new funding type.
|
||||
var pendingChanID [32]byte
|
||||
if _, err := rand.Read(pendingChanID[:]); err != nil {
|
||||
t.Fatalf("unable to gen pending chan ID: %v", err)
|
||||
}
|
||||
_, err = rand.Read(pendingChanID[:])
|
||||
require.NoError(t.t, err)
|
||||
|
||||
// Now that we have the pending channel ID, Dave (our responder) will
|
||||
// register the intent to receive a new channel funding workflow using
|
||||
|
@ -13875,7 +13976,6 @@ func testExternalFundingChanPoint(net *lntest.NetworkHarness, t *harnessTest) {
|
|||
FundingTxidBytes: txid[:],
|
||||
},
|
||||
}
|
||||
thawHeight := uint32(10)
|
||||
chanPointShim := &lnrpc.ChanPointShim{
|
||||
Amt: int64(chanSize),
|
||||
ChanPoint: chanPoint,
|
||||
|
@ -13900,9 +14000,7 @@ func testExternalFundingChanPoint(net *lntest.NetworkHarness, t *harnessTest) {
|
|||
ShimRegister: fundingShim,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unable to walk funding state forward: %v", err)
|
||||
}
|
||||
require.NoError(t.t, err)
|
||||
|
||||
// If we attempt to register the same shim (has the same pending chan
|
||||
// ID), then we should get an error.
|
||||
|
@ -13928,66 +14026,7 @@ func testExternalFundingChanPoint(net *lntest.NetworkHarness, t *harnessTest) {
|
|||
}
|
||||
fundingShim.GetChanPointShim().RemoteKey = daveFundingKey.RawKeyBytes
|
||||
|
||||
// At this point, we'll now carry out the normal basic channel funding
|
||||
// test as everything should now proceed as normal (a regular channel
|
||||
// funding flow).
|
||||
carolChan, daveChan, _, err := basicChannelFundingTest(
|
||||
t, net, carol, dave, fundingShim,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to open channels: %v", err)
|
||||
}
|
||||
|
||||
// Both channels should be marked as frozen with the proper thaw
|
||||
// height.
|
||||
if carolChan.ThawHeight != thawHeight {
|
||||
t.Fatalf("expected thaw height of %v, got %v",
|
||||
carolChan.ThawHeight, thawHeight)
|
||||
}
|
||||
if daveChan.ThawHeight != thawHeight {
|
||||
t.Fatalf("expected thaw height of %v, got %v",
|
||||
daveChan.ThawHeight, thawHeight)
|
||||
}
|
||||
|
||||
// Next, to make sure the channel functions as normal, we'll make some
|
||||
// payments within the channel.
|
||||
payAmt := btcutil.Amount(100000)
|
||||
invoice := &lnrpc.Invoice{
|
||||
Memo: "new chans",
|
||||
Value: int64(payAmt),
|
||||
}
|
||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||
resp, err := dave.AddInvoice(ctxt, invoice)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to add invoice: %v", err)
|
||||
}
|
||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||
err = completePaymentRequests(
|
||||
ctxt, carol, carol.RouterClient, []string{resp.PaymentRequest},
|
||||
true,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to make payments between Carol and Dave")
|
||||
}
|
||||
|
||||
// Now that the channels are open, and we've confirmed that they're
|
||||
// operational, we'll now ensure that the channels are frozen as
|
||||
// intended (if requested).
|
||||
//
|
||||
// First, we'll try to close the channel as Carol, the initiator. This
|
||||
// should fail as a frozen channel only allows the responder to
|
||||
// initiate a channel close.
|
||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||
_, _, err = net.CloseChannel(ctxt, carol, chanPoint, false)
|
||||
if err == nil {
|
||||
t.Fatalf("carol wasn't denied a co-op close attempt for a " +
|
||||
"frozen channel")
|
||||
}
|
||||
|
||||
// Next we'll try but this time with Dave (the responder) as the
|
||||
// initiator. This time the channel should be closed as normal.
|
||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||
closeChannelAndAssert(ctxt, t, net, dave, chanPoint, false)
|
||||
return fundingShim, chanPoint, txid
|
||||
}
|
||||
|
||||
// sendAndAssertSuccess sends the given payment requests and asserts that the
|
||||
|
|
|
@ -492,6 +492,13 @@ func (r *ChannelReservation) IsPsbt() bool {
|
|||
return ok
|
||||
}
|
||||
|
||||
// IsCannedShim returns true if there is a canned shim funding intent mapped to
|
||||
// this reservation.
|
||||
func (r *ChannelReservation) IsCannedShim() bool {
|
||||
_, ok := r.fundingIntent.(*chanfunding.ShimIntent)
|
||||
return ok
|
||||
}
|
||||
|
||||
// ProcessPsbt continues a previously paused funding flow that involves PSBT to
|
||||
// construct the funding transaction. This method can be called once the PSBT is
|
||||
// finalized and the signed transaction is available.
|
||||
|
|
21
rpcserver.go
21
rpcserver.go
|
@ -2358,13 +2358,14 @@ func abandonChanFromGraph(chanGraph *channeldb.ChannelGraph,
|
|||
// AbandonChannel removes all channel state from the database except for a
|
||||
// close summary. This method can be used to get rid of permanently unusable
|
||||
// channels due to bugs fixed in newer versions of lnd.
|
||||
func (r *rpcServer) AbandonChannel(ctx context.Context,
|
||||
func (r *rpcServer) AbandonChannel(_ context.Context,
|
||||
in *lnrpc.AbandonChannelRequest) (*lnrpc.AbandonChannelResponse, error) {
|
||||
|
||||
// If this isn't the dev build, then we won't allow the RPC to be
|
||||
// executed, as it's an advanced feature and won't be activated in
|
||||
// regular production/release builds.
|
||||
if !build.IsDevBuild() {
|
||||
// regular production/release builds except for the explicit case of
|
||||
// externally funded channels that are still pending.
|
||||
if !in.PendingFundingShimOnly && !build.IsDevBuild() {
|
||||
return nil, fmt.Errorf("AbandonChannel RPC call only " +
|
||||
"available in dev builds")
|
||||
}
|
||||
|
@ -2396,6 +2397,20 @@ func (r *rpcServer) AbandonChannel(ctx context.Context,
|
|||
// on-disk state, we'll remove the channel from the switch and peer
|
||||
// state if it's been loaded in.
|
||||
case err == nil:
|
||||
// If the user requested the more safe version that only allows
|
||||
// the removal of externally (shim) funded channels that are
|
||||
// still pending, we enforce this option now that we know the
|
||||
// state of the channel.
|
||||
//
|
||||
// TODO(guggero): Properly store the funding type (wallet, shim,
|
||||
// PSBT) on the channel so we don't need to use the thaw height.
|
||||
isShimFunded := dbChan.ThawHeight > 0
|
||||
isPendingShimFunded := isShimFunded && dbChan.IsPending
|
||||
if in.PendingFundingShimOnly && !isPendingShimFunded {
|
||||
return nil, fmt.Errorf("channel %v is not externally "+
|
||||
"funded or not pending", chanPoint)
|
||||
}
|
||||
|
||||
// We'll mark the channel as borked before we remove the state
|
||||
// from the switch/peer so it won't be loaded back in if the
|
||||
// peer reconnects.
|
||||
|
|
Loading…
Add table
Reference in a new issue