mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-02-23 22:46:40 +01:00
Merge pull request #5356 from guggero/batch-channel-open
Atomic batch channel funding
This commit is contained in:
commit
e76c4c0e9b
22 changed files with 4569 additions and 2415 deletions
|
@ -6,6 +6,7 @@ import (
|
|||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
@ -627,6 +628,167 @@ func openChannelPsbt(rpcCtx context.Context, ctx *cli.Context,
|
|||
}
|
||||
}
|
||||
|
||||
var batchOpenChannelCommand = cli.Command{
|
||||
Name: "batchopenchannel",
|
||||
Category: "Channels",
|
||||
Usage: "Open multiple channels to existing peers in a single " +
|
||||
"transaction.",
|
||||
Description: `
|
||||
Attempt to open one or more new channels to an existing peer with the
|
||||
given node-keys.
|
||||
|
||||
Example:
|
||||
lncli batchopenchannel --sat_per_vbyte=5 '[{
|
||||
"node_pubkey": "02abcdef...",
|
||||
"local_funding_amount": 500000,
|
||||
"private": true,
|
||||
"close_address": "bc1qxxx..."
|
||||
}, {
|
||||
"node_pubkey": "03fedcba...",
|
||||
"local_funding_amount": 200000,
|
||||
"remote_csv_delay": 288
|
||||
}]'
|
||||
|
||||
All nodes listed must already be connected peers, otherwise funding will
|
||||
fail.
|
||||
|
||||
The channel will be initialized with local-amt satoshis local and
|
||||
push-amt satoshis for the remote node. Note that specifying push-amt
|
||||
means you give that amount to the remote node as part of the channel
|
||||
opening. Once the channel is open, a channelPoint (txid:vout) of the
|
||||
funding output is returned.
|
||||
|
||||
If the remote peer supports the option upfront shutdown feature bit
|
||||
(query listpeers to see their supported feature bits), an address to
|
||||
enforce payout of funds on cooperative close can optionally be provided.
|
||||
Note that if you set this value, you will not be able to cooperatively
|
||||
close out to another address.
|
||||
|
||||
One can manually set the fee to be used for the funding transaction via
|
||||
either the --conf_target or --sat_per_vbyte arguments. This is optional.
|
||||
`,
|
||||
ArgsUsage: "channels-json",
|
||||
Flags: []cli.Flag{
|
||||
cli.Int64Flag{
|
||||
Name: "conf_target",
|
||||
Usage: "(optional) the number of blocks that the " +
|
||||
"transaction *should* confirm in, will be " +
|
||||
"used for fee estimation",
|
||||
},
|
||||
cli.Int64Flag{
|
||||
Name: "sat_per_vbyte",
|
||||
Usage: "(optional) a manual fee expressed in " +
|
||||
"sat/vByte that should be used when crafting " +
|
||||
"the transaction",
|
||||
},
|
||||
cli.Uint64Flag{
|
||||
Name: "min_confs",
|
||||
Usage: "(optional) the minimum number of " +
|
||||
"confirmations each one of your outputs used " +
|
||||
"for the funding transaction must satisfy",
|
||||
Value: defaultUtxoMinConf,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "label",
|
||||
Usage: "(optional) a label to attach to the batch " +
|
||||
"transaction when storing it to the local " +
|
||||
"wallet after publishing it",
|
||||
},
|
||||
},
|
||||
Action: actionDecorator(batchOpenChannel),
|
||||
}
|
||||
|
||||
type batchChannelJSON struct {
|
||||
NodePubkey string `json:"node_pubkey,omitempty"`
|
||||
LocalFundingAmount int64 `json:"local_funding_amount,omitempty"`
|
||||
PushSat int64 `json:"push_sat,omitempty"`
|
||||
Private bool `json:"private,omitempty"`
|
||||
MinHtlcMsat int64 `json:"min_htlc_msat,omitempty"`
|
||||
RemoteCsvDelay uint32 `json:"remote_csv_delay,omitempty"`
|
||||
CloseAddress string `json:"close_address,omitempty"`
|
||||
PendingChanID string `json:"pending_chan_id,omitempty"`
|
||||
}
|
||||
|
||||
func batchOpenChannel(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
client, cleanUp := getClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
args := ctx.Args()
|
||||
|
||||
// Show command help if no arguments provided
|
||||
if ctx.NArg() == 0 {
|
||||
_ = cli.ShowCommandHelp(ctx, "batchopenchannel")
|
||||
return nil
|
||||
}
|
||||
|
||||
minConfs := int32(ctx.Uint64("min_confs"))
|
||||
req := &lnrpc.BatchOpenChannelRequest{
|
||||
TargetConf: int32(ctx.Int64("conf_target")),
|
||||
SatPerVbyte: int64(ctx.Uint64("sat_per_vbyte")),
|
||||
MinConfs: minConfs,
|
||||
SpendUnconfirmed: minConfs == 0,
|
||||
Label: ctx.String("label"),
|
||||
}
|
||||
|
||||
// Let's try and parse the JSON part of the CLI now. Fortunately we can
|
||||
// parse it directly into the RPC struct if we use the correct
|
||||
// marshaler that keeps the original snake case.
|
||||
var jsonChannels []*batchChannelJSON
|
||||
if err := json.Unmarshal([]byte(args.First()), &jsonChannels); err != nil {
|
||||
return fmt.Errorf("error parsing channels JSON: %v", err)
|
||||
}
|
||||
|
||||
req.Channels = make([]*lnrpc.BatchOpenChannel, len(jsonChannels))
|
||||
for idx, jsonChannel := range jsonChannels {
|
||||
pubKeyBytes, err := hex.DecodeString(jsonChannel.NodePubkey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing node pubkey hex: %v",
|
||||
err)
|
||||
}
|
||||
pendingChanBytes, err := hex.DecodeString(
|
||||
jsonChannel.PendingChanID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing pending chan ID: %v",
|
||||
err)
|
||||
}
|
||||
|
||||
req.Channels[idx] = &lnrpc.BatchOpenChannel{
|
||||
NodePubkey: pubKeyBytes,
|
||||
LocalFundingAmount: jsonChannel.LocalFundingAmount,
|
||||
PushSat: jsonChannel.PushSat,
|
||||
Private: jsonChannel.Private,
|
||||
MinHtlcMsat: jsonChannel.MinHtlcMsat,
|
||||
RemoteCsvDelay: jsonChannel.RemoteCsvDelay,
|
||||
CloseAddress: jsonChannel.CloseAddress,
|
||||
PendingChanId: pendingChanBytes,
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := client.BatchOpenChannel(ctxc, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, pending := range resp.PendingChannels {
|
||||
txid, err := chainhash.NewHash(pending.Txid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printJSON(struct {
|
||||
FundingTxid string `json:"funding_txid"`
|
||||
FundingOutputIndex uint32 `json:"funding_output_index"`
|
||||
}{
|
||||
FundingTxid: txid.String(),
|
||||
FundingOutputIndex: pending.OutputIndex,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// printChanOpen prints the channel point of the channel open message.
|
||||
func printChanOpen(update *lnrpc.OpenStatusUpdate_ChanOpen) error {
|
||||
channelPoint := update.ChanOpen.ChannelPoint
|
||||
|
|
|
@ -334,6 +334,7 @@ func main() {
|
|||
connectCommand,
|
||||
disconnectCommand,
|
||||
openChannelCommand,
|
||||
batchOpenChannelCommand,
|
||||
closeChannelCommand,
|
||||
closeAllChannelsCommand,
|
||||
abandonChannelCommand,
|
||||
|
|
51
docs/psbt.md
51
docs/psbt.md
|
@ -582,6 +582,22 @@ signature from peer 2 to spend the funds now locked in a 2-of-2 multisig, the
|
|||
fund are lost (unless peer 2 cooperates in a complicated, manual recovery
|
||||
process).
|
||||
|
||||
### Privacy considerations when batch opening channels
|
||||
|
||||
Opening private (non-announced) channels within a transaction that also contains
|
||||
public channels that are announced to the network it is plausible for an outside
|
||||
observer to assume that the non-announced P2WSH output might also be a channel
|
||||
originating from the same node as the public channel. It is therefore
|
||||
recommended to not mix public/private channels within the same batch
|
||||
transaction.
|
||||
|
||||
Batching multiple channels with the same state of the `private` flag can be
|
||||
beneficial for privacy though. Such a transaction can't easily be distinguished
|
||||
from a batch created by Pool or Loop for example. Also, because of the PSBT
|
||||
funding flow, it is also not guaranteed that all channels within such a batch
|
||||
transaction are actually being created for the same node. It is possible to
|
||||
create coin join transactions that create channels for multiple different nodes.
|
||||
|
||||
### Use --no_publish for batch transactions
|
||||
|
||||
To mitigate the problem described in the section above, when open multiple
|
||||
|
@ -589,3 +605,38 @@ channels in one batch transaction, it is **imperative to use the
|
|||
`--no_publish`** flag for each channel but the very last. This prevents the
|
||||
full batch transaction to be published before each and every single channel has
|
||||
fully completed its funding negotiation.
|
||||
|
||||
### Use the BatchOpenChannel RPC for safe batch channel funding
|
||||
|
||||
If `lnd`'s internal wallet should fund the batch channel open transaction then
|
||||
the safest option is the `BatchOpenChannel` RPC (and its
|
||||
`lncli batchopenchannel` counterpart).
|
||||
The `BatchOpenChannel` RPC accepts a list of node pubkeys and amounts and will
|
||||
try to atomically open channels in a single transaction to all of the nodes. If
|
||||
any of the individual channel negotiations fails (for example because of a
|
||||
minimum channel size not being met) then the whole batch is aborted and
|
||||
lingering reservations/intents/pending channels are cleaned up.
|
||||
|
||||
**Example using the CLI**:
|
||||
|
||||
```shell
|
||||
⛰ lncli batchopenchannel --sat_per_vbyte=5 '[{
|
||||
"node_pubkey": "02c95fd94d2a40e483e8a14be1625ad8a82263b37b6a32162170d8d4c13080bedb",
|
||||
"local_funding_amount": 500000,
|
||||
"private": true,
|
||||
"close_address": "2NCJnjD4CZ5JvmkEo1D3QfDM57GX62LUbep"
|
||||
}, {
|
||||
"node_pubkey": "032d57116b92b5f64f022271ebd5e9e23826c0f34ff5ae3e742ad329e0dc5ddff8",
|
||||
"local_funding_amount": 600000,
|
||||
"remote_csv_delay": 288
|
||||
}, {
|
||||
"node_pubkey": "03475f7b07f79672b9a1fd2a3a2350bc444980fe06eb3ae38b132c6f43f958947b",
|
||||
"local_funding_amount": 700000
|
||||
}, {
|
||||
"node_pubkey": "027f013b5cf6b7035744fd8d7d756e05675bf6e829bb75a80be5b9e8e641d20562",
|
||||
"local_funding_amount": 800000
|
||||
}]'
|
||||
```
|
||||
|
||||
**NOTE**: You must be connected to each of the nodes you want to open channels
|
||||
to before you run the command.
|
||||
|
|
|
@ -68,6 +68,15 @@ proposed channel type is used.
|
|||
avoid misleading error messages from dependent services if they use `After`
|
||||
systemd option.
|
||||
|
||||
### Batched channel funding
|
||||
|
||||
[Multiple channels can now be opened in a single
|
||||
transaction](https://github.com/lightningnetwork/lnd/pull/5356) in a safer and
|
||||
more straightforward way by using the `BatchOpenChannel` RPC or the command line
|
||||
version of that RPC called `lncli batchopenchannel`. More information can be
|
||||
found in the [PSBT
|
||||
documentation](../psbt.md#use-the-batchopenchannel-rpc-for-safe-batch-channel-funding).
|
||||
|
||||
## Wallet
|
||||
|
||||
* It is now possible to fund a psbt [without specifying any
|
||||
|
|
529
funding/batch.go
Normal file
529
funding/batch.go
Normal file
|
@ -0,0 +1,529 @@
|
|||
package funding
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/btcsuite/btcutil/psbt"
|
||||
"github.com/lightningnetwork/lnd/labels"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chanfunding"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
var (
|
||||
// errShuttingDown is the error that is returned if a signal on the
|
||||
// quit channel is received which means the whole server is shutting
|
||||
// down.
|
||||
errShuttingDown = errors.New("shutting down")
|
||||
|
||||
// emptyChannelID is a channel ID that consists of all zeros.
|
||||
emptyChannelID = [32]byte{}
|
||||
)
|
||||
|
||||
// batchChannel is a struct that keeps track of a single channel's state within
|
||||
// the batch funding process.
|
||||
type batchChannel struct {
|
||||
fundingReq *InitFundingMsg
|
||||
pendingChanID [32]byte
|
||||
updateChan chan *lnrpc.OpenStatusUpdate
|
||||
errChan chan error
|
||||
fundingAddr string
|
||||
chanPoint *wire.OutPoint
|
||||
isPending bool
|
||||
}
|
||||
|
||||
// processPsbtUpdate processes the first channel update message that is sent
|
||||
// once the initial part of the negotiation has completed and the funding output
|
||||
// (and therefore address) is known.
|
||||
func (c *batchChannel) processPsbtUpdate(u *lnrpc.OpenStatusUpdate) error {
|
||||
psbtUpdate := u.GetPsbtFund()
|
||||
if psbtUpdate == nil {
|
||||
return fmt.Errorf("got unexpected channel update %v", u.Update)
|
||||
}
|
||||
|
||||
if psbtUpdate.FundingAmount != int64(c.fundingReq.LocalFundingAmt) {
|
||||
return fmt.Errorf("got unexpected funding amount %d, wanted "+
|
||||
"%d", psbtUpdate.FundingAmount,
|
||||
c.fundingReq.LocalFundingAmt)
|
||||
}
|
||||
|
||||
c.fundingAddr = psbtUpdate.FundingAddress
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// processPendingUpdate is the second channel update message that is sent once
|
||||
// the negotiation with the peer has completed and the channel is now pending.
|
||||
func (c *batchChannel) processPendingUpdate(u *lnrpc.OpenStatusUpdate) error {
|
||||
pendingUpd := u.GetChanPending()
|
||||
if pendingUpd == nil {
|
||||
return fmt.Errorf("got unexpected channel update %v", u.Update)
|
||||
}
|
||||
|
||||
hash, err := chainhash.NewHash(pendingUpd.Txid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse outpoint TX hash: %v", err)
|
||||
}
|
||||
|
||||
c.chanPoint = &wire.OutPoint{
|
||||
Index: pendingUpd.OutputIndex,
|
||||
Hash: *hash,
|
||||
}
|
||||
c.isPending = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RequestParser is a function that parses an incoming RPC request into the
|
||||
// internal funding initialization message.
|
||||
type RequestParser func(*lnrpc.OpenChannelRequest) (*InitFundingMsg, error)
|
||||
|
||||
// ChannelOpener is a function that kicks off the initial channel open
|
||||
// negotiation with the peer.
|
||||
type ChannelOpener func(*InitFundingMsg) (chan *lnrpc.OpenStatusUpdate,
|
||||
chan error)
|
||||
|
||||
// ChannelAbandoner is a function that can abandon a channel in the local
|
||||
// database, graph and arbitrator state.
|
||||
type ChannelAbandoner func(*wire.OutPoint) error
|
||||
|
||||
// WalletKitServer is a local interface that abstracts away the methods we need
|
||||
// from the wallet kit sub server instance.
|
||||
type WalletKitServer interface {
|
||||
// FundPsbt creates a fully populated PSBT that contains enough inputs
|
||||
// to fund the outputs specified in the template.
|
||||
FundPsbt(context.Context,
|
||||
*walletrpc.FundPsbtRequest) (*walletrpc.FundPsbtResponse, error)
|
||||
|
||||
// FinalizePsbt expects a partial transaction with all inputs and
|
||||
// outputs fully declared and tries to sign all inputs that belong to
|
||||
// the wallet.
|
||||
FinalizePsbt(context.Context,
|
||||
*walletrpc.FinalizePsbtRequest) (*walletrpc.FinalizePsbtResponse,
|
||||
error)
|
||||
|
||||
// ReleaseOutput unlocks an output, allowing it to be available for coin
|
||||
// selection if it remains unspent. The ID should match the one used to
|
||||
// originally lock the output.
|
||||
ReleaseOutput(context.Context,
|
||||
*walletrpc.ReleaseOutputRequest) (*walletrpc.ReleaseOutputResponse,
|
||||
error)
|
||||
}
|
||||
|
||||
// Wallet is a local interface that abstracts away the methods we need from the
|
||||
// internal lightning wallet instance.
|
||||
type Wallet interface {
|
||||
// PsbtFundingVerify looks up a previously registered funding intent by
|
||||
// its pending channel ID and tries to advance the state machine by
|
||||
// verifying the passed PSBT.
|
||||
PsbtFundingVerify([32]byte, *psbt.Packet) error
|
||||
|
||||
// PsbtFundingFinalize looks up a previously registered funding intent
|
||||
// by its pending channel ID and tries to advance the state machine by
|
||||
// finalizing the passed PSBT.
|
||||
PsbtFundingFinalize([32]byte, *psbt.Packet, *wire.MsgTx) error
|
||||
|
||||
// PublishTransaction performs cursory validation (dust checks, etc),
|
||||
// then finally broadcasts the passed transaction to the Bitcoin network.
|
||||
PublishTransaction(*wire.MsgTx, string) error
|
||||
|
||||
// CancelFundingIntent allows a caller to cancel a previously registered
|
||||
// funding intent. If no intent was found, then an error will be
|
||||
// returned.
|
||||
CancelFundingIntent([32]byte) error
|
||||
}
|
||||
|
||||
// BatchConfig is the configuration for executing a single batch transaction for
|
||||
// opening multiple channels atomically.
|
||||
type BatchConfig struct {
|
||||
// RequestParser is the function that parses an incoming RPC request
|
||||
// into the internal funding initialization message.
|
||||
RequestParser RequestParser
|
||||
|
||||
// ChannelOpener is the function that kicks off the initial channel open
|
||||
// negotiation with the peer.
|
||||
ChannelOpener ChannelOpener
|
||||
|
||||
// ChannelAbandoner is the function that can abandon a channel in the
|
||||
// local database, graph and arbitrator state.
|
||||
ChannelAbandoner ChannelAbandoner
|
||||
|
||||
// WalletKitServer is an instance of the wallet kit sub server that can
|
||||
// handle PSBT funding and finalization.
|
||||
WalletKitServer WalletKitServer
|
||||
|
||||
// Wallet is an instance of the internal lightning wallet.
|
||||
Wallet Wallet
|
||||
|
||||
// NetParams contains the current bitcoin network parameters.
|
||||
NetParams *chaincfg.Params
|
||||
|
||||
// Quit is the channel that is selected on to recognize if the main
|
||||
// server is shutting down.
|
||||
Quit chan struct{}
|
||||
}
|
||||
|
||||
// Batcher is a type that can be used to perform an atomic funding of multiple
|
||||
// channels within a single on-chain transaction.
|
||||
type Batcher struct {
|
||||
cfg *BatchConfig
|
||||
|
||||
channels []*batchChannel
|
||||
lockedUTXOs []*walletrpc.UtxoLease
|
||||
|
||||
didPublish bool
|
||||
}
|
||||
|
||||
// NewBatcher returns a new batch channel funding helper.
|
||||
func NewBatcher(cfg *BatchConfig) *Batcher {
|
||||
return &Batcher{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// BatchFund starts the atomic batch channel funding process.
|
||||
//
|
||||
// NOTE: This method should only be called once per instance.
|
||||
func (b *Batcher) BatchFund(ctx context.Context,
|
||||
req *lnrpc.BatchOpenChannelRequest) ([]*lnrpc.PendingUpdate, error) {
|
||||
|
||||
label, err := labels.ValidateAPI(req.Label)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Parse and validate each individual channel.
|
||||
b.channels = make([]*batchChannel, 0, len(req.Channels))
|
||||
for idx, rpcChannel := range req.Channels {
|
||||
// If the user specifies a channel ID, it must be exactly 32
|
||||
// bytes long.
|
||||
if len(rpcChannel.PendingChanId) > 0 &&
|
||||
len(rpcChannel.PendingChanId) != 32 {
|
||||
|
||||
return nil, fmt.Errorf("invalid temp chan ID %x",
|
||||
rpcChannel.PendingChanId)
|
||||
}
|
||||
|
||||
var pendingChanID [32]byte
|
||||
if len(rpcChannel.PendingChanId) == 32 {
|
||||
copy(pendingChanID[:], rpcChannel.PendingChanId)
|
||||
|
||||
// Don't allow the user to be clever by just setting an
|
||||
// all zero channel ID, we need a "real" value here.
|
||||
if pendingChanID == emptyChannelID {
|
||||
return nil, fmt.Errorf("invalid empty temp " +
|
||||
"chan ID")
|
||||
}
|
||||
} else if _, err := rand.Read(pendingChanID[:]); err != nil {
|
||||
return nil, fmt.Errorf("error making temp chan ID: %v",
|
||||
err)
|
||||
}
|
||||
|
||||
fundingReq, err := b.cfg.RequestParser(&lnrpc.OpenChannelRequest{
|
||||
SatPerVbyte: uint64(req.SatPerVbyte),
|
||||
NodePubkey: rpcChannel.NodePubkey,
|
||||
LocalFundingAmount: rpcChannel.LocalFundingAmount,
|
||||
PushSat: rpcChannel.PushSat,
|
||||
TargetConf: req.TargetConf,
|
||||
Private: rpcChannel.Private,
|
||||
MinHtlcMsat: rpcChannel.MinHtlcMsat,
|
||||
RemoteCsvDelay: rpcChannel.RemoteCsvDelay,
|
||||
MinConfs: req.MinConfs,
|
||||
SpendUnconfirmed: req.SpendUnconfirmed,
|
||||
CloseAddress: rpcChannel.CloseAddress,
|
||||
CommitmentType: rpcChannel.CommitmentType,
|
||||
FundingShim: &lnrpc.FundingShim{
|
||||
Shim: &lnrpc.FundingShim_PsbtShim{
|
||||
PsbtShim: &lnrpc.PsbtShim{
|
||||
PendingChanId: pendingChanID[:],
|
||||
NoPublish: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing channel %d: %v",
|
||||
idx, err)
|
||||
}
|
||||
|
||||
// Prepare the stuff that we'll need for the internal PSBT
|
||||
// funding.
|
||||
fundingReq.PendingChanID = pendingChanID
|
||||
fundingReq.ChanFunder = chanfunding.NewPsbtAssembler(
|
||||
btcutil.Amount(rpcChannel.LocalFundingAmount), nil,
|
||||
b.cfg.NetParams, false,
|
||||
)
|
||||
|
||||
b.channels = append(b.channels, &batchChannel{
|
||||
pendingChanID: pendingChanID,
|
||||
fundingReq: fundingReq,
|
||||
})
|
||||
}
|
||||
|
||||
// From this point on we can fail for any of the channels and for any
|
||||
// number of reasons. This deferred function makes sure that the full
|
||||
// operation is actually atomic: We either succeed and publish a
|
||||
// transaction for the full batch or we clean up everything.
|
||||
defer b.cleanup(ctx)
|
||||
|
||||
// Now that we know the user input is sane, we need to kick off the
|
||||
// channel funding negotiation with the peers. Because we specified a
|
||||
// PSBT assembler, we'll get a special response in the channel once the
|
||||
// funding output script is known (which we need to craft the TX).
|
||||
eg := &errgroup.Group{}
|
||||
for _, channel := range b.channels {
|
||||
channel.updateChan, channel.errChan = b.cfg.ChannelOpener(
|
||||
channel.fundingReq,
|
||||
)
|
||||
|
||||
// Launch a goroutine that waits for the initial response on
|
||||
// either the update or error chan.
|
||||
channel := channel
|
||||
eg.Go(func() error {
|
||||
return b.waitForUpdate(channel, true)
|
||||
})
|
||||
}
|
||||
|
||||
// Wait for all goroutines to report back. Any error at this stage means
|
||||
// we need to abort.
|
||||
if err := eg.Wait(); err != nil {
|
||||
return nil, fmt.Errorf("error batch opening channel, initial "+
|
||||
"negotiation failed: %v", err)
|
||||
}
|
||||
|
||||
// We can now assemble all outputs that we're going to give to the PSBT
|
||||
// funding method of the wallet kit server.
|
||||
txTemplate := &walletrpc.TxTemplate{
|
||||
Outputs: make(map[string]uint64),
|
||||
}
|
||||
for _, channel := range b.channels {
|
||||
txTemplate.Outputs[channel.fundingAddr] = uint64(
|
||||
channel.fundingReq.LocalFundingAmt,
|
||||
)
|
||||
}
|
||||
|
||||
// Great, we've now started the channel negotiation successfully with
|
||||
// all peers. This means we know the channel outputs for all channels
|
||||
// and can craft our PSBT now. We take the fee rate and min conf
|
||||
// settings from the first request as all of them should be equal
|
||||
// anyway.
|
||||
firstReq := b.channels[0].fundingReq
|
||||
feeRateSatPerKVByte := firstReq.FundingFeePerKw.FeePerKVByte()
|
||||
fundPsbtReq := &walletrpc.FundPsbtRequest{
|
||||
Template: &walletrpc.FundPsbtRequest_Raw{
|
||||
Raw: txTemplate,
|
||||
},
|
||||
Fees: &walletrpc.FundPsbtRequest_SatPerVbyte{
|
||||
SatPerVbyte: uint64(feeRateSatPerKVByte) / 1000,
|
||||
},
|
||||
MinConfs: firstReq.MinConfs,
|
||||
SpendUnconfirmed: firstReq.MinConfs == 0,
|
||||
}
|
||||
fundPsbtResp, err := b.cfg.WalletKitServer.FundPsbt(ctx, fundPsbtReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error funding PSBT for batch channel "+
|
||||
"open: %v", err)
|
||||
}
|
||||
|
||||
// Funding was successful. This means there are some UTXOs that are now
|
||||
// locked for us. We need to make sure we release them if we don't
|
||||
// complete the publish process.
|
||||
b.lockedUTXOs = fundPsbtResp.LockedUtxos
|
||||
|
||||
// Parse and log the funded PSBT for debugging purposes.
|
||||
unsignedPacket, err := psbt.NewFromRawBytes(
|
||||
bytes.NewReader(fundPsbtResp.FundedPsbt), false,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing funded PSBT for batch "+
|
||||
"channel open: %v", err)
|
||||
}
|
||||
log.Tracef("[batchopenchannel] funded PSBT: %s",
|
||||
base64.StdEncoding.EncodeToString(fundPsbtResp.FundedPsbt))
|
||||
|
||||
// With the funded PSBT we can now advance the funding state machine of
|
||||
// each of the channels.
|
||||
for _, channel := range b.channels {
|
||||
err = b.cfg.Wallet.PsbtFundingVerify(
|
||||
channel.pendingChanID, unsignedPacket,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error verifying PSBT: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// The funded PSBT was accepted by each of the assemblers, let's now
|
||||
// sign/finalize it.
|
||||
finalizePsbtResp, err := b.cfg.WalletKitServer.FinalizePsbt(
|
||||
ctx, &walletrpc.FinalizePsbtRequest{
|
||||
FundedPsbt: fundPsbtResp.FundedPsbt,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error finalizing PSBT for batch "+
|
||||
"channel open: %v", err)
|
||||
}
|
||||
finalTx := &wire.MsgTx{}
|
||||
txReader := bytes.NewReader(finalizePsbtResp.RawFinalTx)
|
||||
if err := finalTx.Deserialize(txReader); err != nil {
|
||||
return nil, fmt.Errorf("error parsing signed raw TX: %v", err)
|
||||
}
|
||||
log.Tracef("[batchopenchannel] signed PSBT: %s",
|
||||
base64.StdEncoding.EncodeToString(finalizePsbtResp.SignedPsbt))
|
||||
|
||||
// Advance the funding state machine of each of the channels a last time
|
||||
// to complete the negotiation with the now signed funding TX.
|
||||
for _, channel := range b.channels {
|
||||
err = b.cfg.Wallet.PsbtFundingFinalize(
|
||||
channel.pendingChanID, nil, finalTx,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error finalizing PSBT: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Now every channel should be ready for the funding transaction to be
|
||||
// broadcast. Let's wait for the updates that actually confirm this
|
||||
// state.
|
||||
eg = &errgroup.Group{}
|
||||
for _, channel := range b.channels {
|
||||
// Launch another goroutine that waits for the channel pending
|
||||
// response on the update chan.
|
||||
channel := channel
|
||||
eg.Go(func() error {
|
||||
return b.waitForUpdate(channel, false)
|
||||
})
|
||||
}
|
||||
|
||||
// Wait for all updates and make sure we're still good to proceed.
|
||||
if err := eg.Wait(); err != nil {
|
||||
return nil, fmt.Errorf("error batch opening channel, final "+
|
||||
"negotiation failed: %v", err)
|
||||
}
|
||||
|
||||
// Great, we're now finally ready to publish the transaction.
|
||||
err = b.cfg.Wallet.PublishTransaction(finalTx, label)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error publishing final batch "+
|
||||
"transaction: %v", err)
|
||||
}
|
||||
b.didPublish = true
|
||||
|
||||
rpcPoints := make([]*lnrpc.PendingUpdate, len(b.channels))
|
||||
for idx, channel := range b.channels {
|
||||
rpcPoints[idx] = &lnrpc.PendingUpdate{
|
||||
Txid: channel.chanPoint.Hash.CloneBytes(),
|
||||
OutputIndex: channel.chanPoint.Index,
|
||||
}
|
||||
}
|
||||
|
||||
return rpcPoints, nil
|
||||
}
|
||||
|
||||
// waitForUpdate waits for an incoming channel update (or error) for a single
|
||||
// channel.
|
||||
//
|
||||
// NOTE: Must be called in a goroutine as this blocks until an update or error
|
||||
// is received.
|
||||
func (b *Batcher) waitForUpdate(channel *batchChannel, firstUpdate bool) error {
|
||||
select {
|
||||
// If an error occurs then immediately return the error to the client.
|
||||
case err := <-channel.errChan:
|
||||
log.Errorf("unable to open channel to NodeKey(%x): %v",
|
||||
channel.fundingReq.TargetPubkey.SerializeCompressed(),
|
||||
err)
|
||||
return err
|
||||
|
||||
// Otherwise, wait for the next channel update. The first update sent
|
||||
// must be the signal to start the PSBT funding in our case since we
|
||||
// specified a PSBT shim. The second update will be the signal that the
|
||||
// channel is now pending.
|
||||
case fundingUpdate := <-channel.updateChan:
|
||||
log.Tracef("[batchopenchannel] received update: %v",
|
||||
fundingUpdate)
|
||||
|
||||
// Depending on what update we were waiting for the batch
|
||||
// channel knows what to do with it.
|
||||
if firstUpdate {
|
||||
return channel.processPsbtUpdate(fundingUpdate)
|
||||
}
|
||||
|
||||
return channel.processPendingUpdate(fundingUpdate)
|
||||
|
||||
case <-b.cfg.Quit:
|
||||
return errShuttingDown
|
||||
}
|
||||
}
|
||||
|
||||
// cleanup tries to remove any pending state or UTXO locks in case we had to
|
||||
// abort before finalizing and publishing the funding transaction.
|
||||
func (b *Batcher) cleanup(ctx context.Context) {
|
||||
// Did we publish a transaction? Then there's nothing to clean up since
|
||||
// we succeeded.
|
||||
if b.didPublish {
|
||||
return
|
||||
}
|
||||
|
||||
// Make sure the error message doesn't sound too scary. These might be
|
||||
// logged quite frequently depending on where exactly things were
|
||||
// aborted. We could just not log any cleanup errors though it might be
|
||||
// helpful to debug things if something doesn't go as expected.
|
||||
const errMsgTpl = "Attempted to clean up after failed batch channel " +
|
||||
"open but could not %s: %v"
|
||||
|
||||
// If we failed, we clean up in reverse order. First, let's unlock the
|
||||
// leased outputs.
|
||||
for _, lockedUTXO := range b.lockedUTXOs {
|
||||
rpcOP := &lnrpc.OutPoint{
|
||||
OutputIndex: lockedUTXO.Outpoint.OutputIndex,
|
||||
TxidBytes: lockedUTXO.Outpoint.TxidBytes,
|
||||
}
|
||||
_, err := b.cfg.WalletKitServer.ReleaseOutput(
|
||||
ctx, &walletrpc.ReleaseOutputRequest{
|
||||
Id: lockedUTXO.Id,
|
||||
Outpoint: rpcOP,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
log.Debugf(errMsgTpl, "release locked output "+
|
||||
lockedUTXO.Outpoint.String(), err)
|
||||
}
|
||||
}
|
||||
|
||||
// Then go through all channels that ever got into a pending state and
|
||||
// remove the pending channel by abandoning them.
|
||||
for _, channel := range b.channels {
|
||||
if !channel.isPending {
|
||||
continue
|
||||
}
|
||||
|
||||
err := b.cfg.ChannelAbandoner(channel.chanPoint)
|
||||
if err != nil {
|
||||
log.Debugf(errMsgTpl, "abandon pending open channel",
|
||||
err)
|
||||
}
|
||||
}
|
||||
|
||||
// And finally clean up the funding shim for each channel that didn't
|
||||
// make it into a pending state.
|
||||
for _, channel := range b.channels {
|
||||
if channel.isPending {
|
||||
continue
|
||||
}
|
||||
|
||||
err := b.cfg.Wallet.CancelFundingIntent(channel.pendingChanID)
|
||||
if err != nil {
|
||||
log.Debugf(errMsgTpl, "cancel funding shim", err)
|
||||
}
|
||||
}
|
||||
}
|
422
funding/batch_test.go
Normal file
422
funding/batch_test.go
Normal file
|
@ -0,0 +1,422 @@
|
|||
package funding
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/btcsuite/btcutil/psbt"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
errFundingFailed = errors.New("funding failed")
|
||||
|
||||
testPubKey1Hex = "02e1ce77dfdda9fd1cf5e9d796faf57d1cedef9803aec84a6d7" +
|
||||
"f8487d32781341e"
|
||||
testPubKey1Bytes, _ = hex.DecodeString(testPubKey1Hex)
|
||||
|
||||
testPubKey2Hex = "039ddfc912035417b24aefe8da155267d71c3cf9e35405fc390" +
|
||||
"df8357c5da7a5eb"
|
||||
testPubKey2Bytes, _ = hex.DecodeString(testPubKey2Hex)
|
||||
|
||||
testOutPoint = wire.OutPoint{
|
||||
Hash: [32]byte{1, 2, 3},
|
||||
Index: 2,
|
||||
}
|
||||
)
|
||||
|
||||
type fundingIntent struct {
|
||||
chanIndex uint32
|
||||
updateChan chan *lnrpc.OpenStatusUpdate
|
||||
errChan chan error
|
||||
}
|
||||
|
||||
type testHarness struct {
|
||||
t *testing.T
|
||||
batcher *Batcher
|
||||
|
||||
failUpdate1 bool
|
||||
failUpdate2 bool
|
||||
failPublish bool
|
||||
|
||||
intentsCreated map[[32]byte]*fundingIntent
|
||||
intentsCanceled map[[32]byte]struct{}
|
||||
abandonedChannels map[wire.OutPoint]struct{}
|
||||
releasedUTXOs map[wire.OutPoint]struct{}
|
||||
|
||||
pendingPacket *psbt.Packet
|
||||
pendingTx *wire.MsgTx
|
||||
|
||||
txPublished bool
|
||||
}
|
||||
|
||||
func newTestHarness(t *testing.T, failUpdate1, failUpdate2,
|
||||
failPublish bool) *testHarness {
|
||||
|
||||
h := &testHarness{
|
||||
t: t,
|
||||
failUpdate1: failUpdate1,
|
||||
failUpdate2: failUpdate2,
|
||||
failPublish: failPublish,
|
||||
intentsCreated: make(map[[32]byte]*fundingIntent),
|
||||
intentsCanceled: make(map[[32]byte]struct{}),
|
||||
abandonedChannels: make(map[wire.OutPoint]struct{}),
|
||||
releasedUTXOs: make(map[wire.OutPoint]struct{}),
|
||||
pendingTx: &wire.MsgTx{
|
||||
Version: 2,
|
||||
TxIn: []*wire.TxIn{{
|
||||
// Our one input that pays for everything.
|
||||
PreviousOutPoint: testOutPoint,
|
||||
}},
|
||||
TxOut: []*wire.TxOut{{
|
||||
// Our static change output.
|
||||
PkScript: []byte{1, 2, 3},
|
||||
Value: 99,
|
||||
}},
|
||||
},
|
||||
}
|
||||
h.batcher = NewBatcher(&BatchConfig{
|
||||
RequestParser: h.parseRequest,
|
||||
ChannelOpener: h.openChannel,
|
||||
ChannelAbandoner: h.abandonChannel,
|
||||
WalletKitServer: h,
|
||||
Wallet: h,
|
||||
Quit: make(chan struct{}),
|
||||
})
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *testHarness) parseRequest(
|
||||
in *lnrpc.OpenChannelRequest) (*InitFundingMsg, error) {
|
||||
|
||||
pubKey, err := btcec.ParsePubKey(in.NodePubkey, btcec.S256())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &InitFundingMsg{
|
||||
TargetPubkey: pubKey,
|
||||
LocalFundingAmt: btcutil.Amount(in.LocalFundingAmount),
|
||||
PushAmt: lnwire.NewMSatFromSatoshis(
|
||||
btcutil.Amount(in.PushSat),
|
||||
),
|
||||
FundingFeePerKw: chainfee.SatPerKVByte(
|
||||
in.SatPerVbyte * 1000,
|
||||
).FeePerKWeight(),
|
||||
Private: in.Private,
|
||||
RemoteCsvDelay: uint16(in.RemoteCsvDelay),
|
||||
MinConfs: in.MinConfs,
|
||||
MaxLocalCsv: uint16(in.MaxLocalCsv),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *testHarness) openChannel(
|
||||
req *InitFundingMsg) (chan *lnrpc.OpenStatusUpdate, chan error) {
|
||||
|
||||
updateChan := make(chan *lnrpc.OpenStatusUpdate, 2)
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
// The change output is always index 0.
|
||||
chanIndex := uint32(len(h.intentsCreated) + 1)
|
||||
|
||||
h.intentsCreated[req.PendingChanID] = &fundingIntent{
|
||||
chanIndex: chanIndex,
|
||||
updateChan: updateChan,
|
||||
errChan: errChan,
|
||||
}
|
||||
h.pendingTx.TxOut = append(h.pendingTx.TxOut, &wire.TxOut{
|
||||
PkScript: []byte{1, 2, 3, byte(chanIndex)},
|
||||
Value: int64(req.LocalFundingAmt),
|
||||
})
|
||||
|
||||
if h.failUpdate1 {
|
||||
errChan <- errFundingFailed
|
||||
|
||||
// Once we fail we don't send any more updates.
|
||||
return updateChan, errChan
|
||||
}
|
||||
|
||||
updateChan <- &lnrpc.OpenStatusUpdate{
|
||||
PendingChanId: req.PendingChanID[:],
|
||||
Update: &lnrpc.OpenStatusUpdate_PsbtFund{
|
||||
PsbtFund: &lnrpc.ReadyForPsbtFunding{
|
||||
FundingAmount: int64(
|
||||
req.LocalFundingAmt,
|
||||
),
|
||||
FundingAddress: fmt.Sprintf("foo%d", chanIndex),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return updateChan, errChan
|
||||
}
|
||||
|
||||
func (h *testHarness) abandonChannel(op *wire.OutPoint) error {
|
||||
h.abandonedChannels[*op] = struct{}{}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *testHarness) FundPsbt(context.Context,
|
||||
*walletrpc.FundPsbtRequest) (*walletrpc.FundPsbtResponse, error) {
|
||||
|
||||
packet, err := psbt.NewFromUnsignedTx(h.pendingTx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.pendingPacket = packet
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := packet.Serialize(&buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &walletrpc.FundPsbtResponse{
|
||||
FundedPsbt: buf.Bytes(),
|
||||
LockedUtxos: []*walletrpc.UtxoLease{{
|
||||
Id: []byte{1, 2, 3},
|
||||
Outpoint: &lnrpc.OutPoint{
|
||||
TxidBytes: testOutPoint.Hash[:],
|
||||
OutputIndex: testOutPoint.Index,
|
||||
},
|
||||
}},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *testHarness) FinalizePsbt(context.Context,
|
||||
*walletrpc.FinalizePsbtRequest) (*walletrpc.FinalizePsbtResponse,
|
||||
error) {
|
||||
|
||||
var psbtBuf bytes.Buffer
|
||||
if err := h.pendingPacket.Serialize(&psbtBuf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var txBuf bytes.Buffer
|
||||
if err := h.pendingTx.Serialize(&txBuf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &walletrpc.FinalizePsbtResponse{
|
||||
SignedPsbt: psbtBuf.Bytes(),
|
||||
RawFinalTx: txBuf.Bytes(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *testHarness) ReleaseOutput(_ context.Context,
|
||||
r *walletrpc.ReleaseOutputRequest) (*walletrpc.ReleaseOutputResponse,
|
||||
error) {
|
||||
|
||||
hash, err := chainhash.NewHash(r.Outpoint.TxidBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
op := wire.OutPoint{
|
||||
Hash: *hash,
|
||||
Index: r.Outpoint.OutputIndex,
|
||||
}
|
||||
|
||||
h.releasedUTXOs[op] = struct{}{}
|
||||
|
||||
return &walletrpc.ReleaseOutputResponse{}, nil
|
||||
}
|
||||
|
||||
func (h *testHarness) PsbtFundingVerify([32]byte, *psbt.Packet) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *testHarness) PsbtFundingFinalize(pid [32]byte, _ *psbt.Packet,
|
||||
_ *wire.MsgTx) error {
|
||||
|
||||
// During the finalize phase we can now prepare the next update to send.
|
||||
// For this we first need to find the intent that has the channels we
|
||||
// need to send on.
|
||||
intent, ok := h.intentsCreated[pid]
|
||||
if !ok {
|
||||
return fmt.Errorf("intent %x not found", pid)
|
||||
}
|
||||
|
||||
// We should now also have the final TX, let's get its hash.
|
||||
hash := h.pendingTx.TxHash()
|
||||
|
||||
// For the second update we fail on the second channel only so the first
|
||||
// is actually pending.
|
||||
if h.failUpdate2 && intent.chanIndex == 2 {
|
||||
intent.errChan <- errFundingFailed
|
||||
} else {
|
||||
intent.updateChan <- &lnrpc.OpenStatusUpdate{
|
||||
PendingChanId: pid[:],
|
||||
Update: &lnrpc.OpenStatusUpdate_ChanPending{
|
||||
ChanPending: &lnrpc.PendingUpdate{
|
||||
Txid: hash[:],
|
||||
OutputIndex: intent.chanIndex,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *testHarness) PublishTransaction(*wire.MsgTx, string) error {
|
||||
if h.failPublish {
|
||||
return errFundingFailed
|
||||
}
|
||||
|
||||
h.txPublished = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *testHarness) CancelFundingIntent(pid [32]byte) error {
|
||||
h.intentsCanceled[pid] = struct{}{}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestBatchFund tests different success and error scenarios of the atomic batch
|
||||
// channel funding.
|
||||
func TestBatchFund(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
failUpdate1 bool
|
||||
failUpdate2 bool
|
||||
failPublish bool
|
||||
channels []*lnrpc.BatchOpenChannel
|
||||
expectedErr string
|
||||
}{{
|
||||
name: "happy path",
|
||||
channels: []*lnrpc.BatchOpenChannel{{
|
||||
NodePubkey: testPubKey1Bytes,
|
||||
LocalFundingAmount: 1234,
|
||||
}, {
|
||||
NodePubkey: testPubKey2Bytes,
|
||||
LocalFundingAmount: 4321,
|
||||
}},
|
||||
}, {
|
||||
name: "initial negotiation failure",
|
||||
failUpdate1: true,
|
||||
channels: []*lnrpc.BatchOpenChannel{{
|
||||
NodePubkey: testPubKey1Bytes,
|
||||
LocalFundingAmount: 1234,
|
||||
}, {
|
||||
NodePubkey: testPubKey2Bytes,
|
||||
LocalFundingAmount: 4321,
|
||||
}},
|
||||
expectedErr: "initial negotiation failed",
|
||||
}, {
|
||||
name: "final negotiation failure",
|
||||
failUpdate2: true,
|
||||
channels: []*lnrpc.BatchOpenChannel{{
|
||||
NodePubkey: testPubKey1Bytes,
|
||||
LocalFundingAmount: 1234,
|
||||
}, {
|
||||
NodePubkey: testPubKey2Bytes,
|
||||
LocalFundingAmount: 4321,
|
||||
}},
|
||||
expectedErr: "final negotiation failed",
|
||||
}, {
|
||||
name: "publish failure",
|
||||
failPublish: true,
|
||||
channels: []*lnrpc.BatchOpenChannel{{
|
||||
NodePubkey: testPubKey1Bytes,
|
||||
LocalFundingAmount: 1234,
|
||||
}, {
|
||||
NodePubkey: testPubKey2Bytes,
|
||||
LocalFundingAmount: 4321,
|
||||
}},
|
||||
expectedErr: "error publishing final batch transaction",
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
h := newTestHarness(
|
||||
t, tc.failUpdate1, tc.failUpdate2,
|
||||
tc.failPublish,
|
||||
)
|
||||
|
||||
req := &lnrpc.BatchOpenChannelRequest{
|
||||
Channels: tc.channels,
|
||||
SatPerVbyte: 5,
|
||||
MinConfs: 1,
|
||||
}
|
||||
updates, err := h.batcher.BatchFund(
|
||||
context.Background(), req,
|
||||
)
|
||||
|
||||
if tc.failUpdate1 || tc.failUpdate2 || tc.failPublish {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tc.expectedErr)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Len(t, updates, len(tc.channels))
|
||||
}
|
||||
|
||||
if tc.failUpdate1 {
|
||||
require.Len(t, h.releasedUTXOs, 0)
|
||||
require.Len(t, h.intentsCreated, 2)
|
||||
for pid := range h.intentsCreated {
|
||||
require.Contains(
|
||||
t, h.intentsCanceled, pid,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
hash := h.pendingTx.TxHash()
|
||||
if tc.failUpdate2 {
|
||||
require.Len(t, h.releasedUTXOs, 1)
|
||||
require.Len(t, h.intentsCreated, 2)
|
||||
|
||||
// If we fail on update 2 we do so on the second
|
||||
// channel so one will be pending and one not
|
||||
// yet.
|
||||
require.Len(t, h.intentsCanceled, 1)
|
||||
require.Len(t, h.abandonedChannels, 1)
|
||||
require.Contains(
|
||||
t, h.abandonedChannels, wire.OutPoint{
|
||||
Hash: hash,
|
||||
Index: 1,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if tc.failPublish {
|
||||
require.Len(t, h.releasedUTXOs, 1)
|
||||
require.Len(t, h.intentsCreated, 2)
|
||||
|
||||
require.Len(t, h.intentsCanceled, 0)
|
||||
require.Len(t, h.abandonedChannels, 2)
|
||||
require.Contains(
|
||||
t, h.abandonedChannels, wire.OutPoint{
|
||||
Hash: hash,
|
||||
Index: 1,
|
||||
},
|
||||
)
|
||||
require.Contains(
|
||||
t, h.abandonedChannels, wire.OutPoint{
|
||||
Hash: hash,
|
||||
Index: 2,
|
||||
},
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
1
go.mod
1
go.mod
|
@ -57,6 +57,7 @@ require (
|
|||
go.etcd.io/etcd/client/v3 v3.5.0
|
||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba
|
||||
google.golang.org/grpc v1.38.0
|
||||
google.golang.org/protobuf v1.26.0
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -716,6 +716,40 @@ func request_Lightning_OpenChannel_0(ctx context.Context, marshaler runtime.Mars
|
|||
|
||||
}
|
||||
|
||||
func request_Lightning_BatchOpenChannel_0(ctx context.Context, marshaler runtime.Marshaler, client LightningClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq BatchOpenChannelRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
newReader, berr := utilities.IOReaderFactory(req.Body)
|
||||
if berr != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
|
||||
}
|
||||
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
|
||||
msg, err := client.BatchOpenChannel(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func local_request_Lightning_BatchOpenChannel_0(ctx context.Context, marshaler runtime.Marshaler, server LightningServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq BatchOpenChannelRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
newReader, berr := utilities.IOReaderFactory(req.Body)
|
||||
if berr != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
|
||||
}
|
||||
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
|
||||
msg, err := server.BatchOpenChannel(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func request_Lightning_FundingStateStep_0(ctx context.Context, marshaler runtime.Marshaler, client LightningClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq FundingTransitionMsg
|
||||
var metadata runtime.ServerMetadata
|
||||
|
@ -2618,6 +2652,29 @@ func RegisterLightningHandlerServer(ctx context.Context, mux *runtime.ServeMux,
|
|||
return
|
||||
})
|
||||
|
||||
mux.Handle("POST", pattern_Lightning_BatchOpenChannel_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
var stream runtime.ServerTransportStream
|
||||
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/lnrpc.Lightning/BatchOpenChannel", runtime.WithHTTPPathPattern("/v1/channels/batch"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_Lightning_BatchOpenChannel_0(rctx, inboundMarshaler, server, req, pathParams)
|
||||
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
|
||||
ctx = runtime.NewServerMetadataContext(ctx, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_Lightning_BatchOpenChannel_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("POST", pattern_Lightning_FundingStateStep_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
|
@ -3828,6 +3885,26 @@ func RegisterLightningHandlerClient(ctx context.Context, mux *runtime.ServeMux,
|
|||
|
||||
})
|
||||
|
||||
mux.Handle("POST", pattern_Lightning_BatchOpenChannel_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
rctx, err := runtime.AnnotateContext(ctx, mux, req, "/lnrpc.Lightning/BatchOpenChannel", runtime.WithHTTPPathPattern("/v1/channels/batch"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_Lightning_BatchOpenChannel_0(rctx, inboundMarshaler, client, req, pathParams)
|
||||
ctx = runtime.NewServerMetadataContext(ctx, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_Lightning_BatchOpenChannel_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("POST", pattern_Lightning_FundingStateStep_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
|
@ -4578,6 +4655,8 @@ var (
|
|||
|
||||
pattern_Lightning_OpenChannel_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "channels", "stream"}, ""))
|
||||
|
||||
pattern_Lightning_BatchOpenChannel_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "channels", "batch"}, ""))
|
||||
|
||||
pattern_Lightning_FundingStateStep_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "funding", "step"}, ""))
|
||||
|
||||
pattern_Lightning_ChannelAcceptor_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "channels", "acceptor"}, ""))
|
||||
|
@ -4696,6 +4775,8 @@ var (
|
|||
|
||||
forward_Lightning_OpenChannel_0 = runtime.ForwardResponseStream
|
||||
|
||||
forward_Lightning_BatchOpenChannel_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_Lightning_FundingStateStep_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_Lightning_ChannelAcceptor_0 = runtime.ForwardResponseStream
|
||||
|
|
|
@ -666,6 +666,31 @@ func RegisterLightningJSONCallbacks(registry map[string]func(ctx context.Context
|
|||
}()
|
||||
}
|
||||
|
||||
registry["lnrpc.Lightning.BatchOpenChannel"] = func(ctx context.Context,
|
||||
conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {
|
||||
|
||||
req := &BatchOpenChannelRequest{}
|
||||
err := marshaler.Unmarshal([]byte(reqJSON), req)
|
||||
if err != nil {
|
||||
callback("", err)
|
||||
return
|
||||
}
|
||||
|
||||
client := NewLightningClient(conn)
|
||||
resp, err := client.BatchOpenChannel(ctx, req)
|
||||
if err != nil {
|
||||
callback("", err)
|
||||
return
|
||||
}
|
||||
|
||||
respBytes, err := marshaler.Marshal(resp)
|
||||
if err != nil {
|
||||
callback("", err)
|
||||
return
|
||||
}
|
||||
callback(string(respBytes), nil)
|
||||
}
|
||||
|
||||
registry["lnrpc.Lightning.FundingStateStep"] = func(ctx context.Context,
|
||||
conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {
|
||||
|
||||
|
|
|
@ -200,6 +200,16 @@ service Lightning {
|
|||
*/
|
||||
rpc OpenChannel (OpenChannelRequest) returns (stream OpenStatusUpdate);
|
||||
|
||||
/* lncli: `batchopenchannel`
|
||||
BatchOpenChannel attempts to open multiple single-funded channels in a
|
||||
single transaction in an atomic way. This means either all channel open
|
||||
requests succeed at once or all attempts are aborted if any of them fail.
|
||||
This is the safer variant of using PSBTs to manually fund a batch of
|
||||
channels through the OpenChannel RPC.
|
||||
*/
|
||||
rpc BatchOpenChannel (BatchOpenChannelRequest)
|
||||
returns (BatchOpenChannelResponse);
|
||||
|
||||
/*
|
||||
FundingStateStep is an advanced funding related call that allows the caller
|
||||
to either execute some preparatory steps for a funding workflow, or
|
||||
|
@ -1776,6 +1786,84 @@ message ReadyForPsbtFunding {
|
|||
bytes psbt = 3;
|
||||
}
|
||||
|
||||
message BatchOpenChannelRequest {
|
||||
// The list of channels to open.
|
||||
repeated BatchOpenChannel channels = 1;
|
||||
|
||||
// The target number of blocks that the funding transaction should be
|
||||
// confirmed by.
|
||||
int32 target_conf = 2;
|
||||
|
||||
// A manual fee rate set in sat/vByte that should be used when crafting the
|
||||
// funding transaction.
|
||||
int64 sat_per_vbyte = 3;
|
||||
|
||||
// The minimum number of confirmations each one of your outputs used for
|
||||
// the funding transaction must satisfy.
|
||||
int32 min_confs = 4;
|
||||
|
||||
// Whether unconfirmed outputs should be used as inputs for the funding
|
||||
// transaction.
|
||||
bool spend_unconfirmed = 5;
|
||||
|
||||
// An optional label for the batch transaction, limited to 500 characters.
|
||||
string label = 6;
|
||||
}
|
||||
|
||||
message BatchOpenChannel {
|
||||
// The pubkey of the node to open a channel with. When using REST, this
|
||||
// field must be encoded as base64.
|
||||
bytes node_pubkey = 1;
|
||||
|
||||
// The number of satoshis the wallet should commit to the channel.
|
||||
int64 local_funding_amount = 2;
|
||||
|
||||
// The number of satoshis to push to the remote side as part of the initial
|
||||
// commitment state.
|
||||
int64 push_sat = 3;
|
||||
|
||||
// Whether this channel should be private, not announced to the greater
|
||||
// network.
|
||||
bool private = 4;
|
||||
|
||||
// The minimum value in millisatoshi we will require for incoming HTLCs on
|
||||
// the channel.
|
||||
int64 min_htlc_msat = 5;
|
||||
|
||||
// The delay we require on the remote's commitment transaction. If this is
|
||||
// not set, it will be scaled automatically with the channel size.
|
||||
uint32 remote_csv_delay = 6;
|
||||
|
||||
/*
|
||||
Close address is an optional address which specifies the address to which
|
||||
funds should be paid out to upon cooperative close. This field may only be
|
||||
set if the peer supports the option upfront feature bit (call listpeers
|
||||
to check). The remote peer will only accept cooperative closes to this
|
||||
address if it is set.
|
||||
|
||||
Note: If this value is set on channel creation, you will *not* be able to
|
||||
cooperatively close out to a different address.
|
||||
*/
|
||||
string close_address = 7;
|
||||
|
||||
/*
|
||||
An optional, unique identifier of 32 random bytes that will be used as the
|
||||
pending channel ID to identify the channel while it is in the pre-pending
|
||||
state.
|
||||
*/
|
||||
bytes pending_chan_id = 8;
|
||||
|
||||
/*
|
||||
The explicit commitment type to use. Note this field will only be used if
|
||||
the remote peer supports explicit channel negotiation.
|
||||
*/
|
||||
CommitmentType commitment_type = 9;
|
||||
}
|
||||
|
||||
message BatchOpenChannelResponse {
|
||||
repeated PendingUpdate pending_channels = 1;
|
||||
}
|
||||
|
||||
message OpenChannelRequest {
|
||||
// A manual fee rate set in sat/vbyte that should be used when crafting the
|
||||
// funding transaction.
|
||||
|
|
|
@ -423,6 +423,39 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/v1/channels/batch": {
|
||||
"post": {
|
||||
"summary": "lncli: `batchopenchannel`\nBatchOpenChannel attempts to open multiple single-funded channels in a\nsingle transaction in an atomic way. This means either all channel open\nrequests succeed at once or all attempts are aborted if any of them fail.\nThis is the safer variant of using PSBTs to manually fund a batch of\nchannels through the OpenChannel RPC.",
|
||||
"operationId": "Lightning_BatchOpenChannel",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A successful response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/lnrpcBatchOpenChannelResponse"
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "An unexpected error response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/rpcStatus"
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/lnrpcBatchOpenChannelRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Lightning"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v1/channels/closed": {
|
||||
"get": {
|
||||
"summary": "lncli: `closedchannels`\nClosedChannels returns a description of all the closed channels that\nthis node was a participant in.",
|
||||
|
@ -2726,6 +2759,99 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"lnrpcBatchOpenChannel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"node_pubkey": {
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"description": "The pubkey of the node to open a channel with. When using REST, this\nfield must be encoded as base64."
|
||||
},
|
||||
"local_funding_amount": {
|
||||
"type": "string",
|
||||
"format": "int64",
|
||||
"description": "The number of satoshis the wallet should commit to the channel."
|
||||
},
|
||||
"push_sat": {
|
||||
"type": "string",
|
||||
"format": "int64",
|
||||
"description": "The number of satoshis to push to the remote side as part of the initial\ncommitment state."
|
||||
},
|
||||
"private": {
|
||||
"type": "boolean",
|
||||
"description": "Whether this channel should be private, not announced to the greater\nnetwork."
|
||||
},
|
||||
"min_htlc_msat": {
|
||||
"type": "string",
|
||||
"format": "int64",
|
||||
"description": "The minimum value in millisatoshi we will require for incoming HTLCs on\nthe channel."
|
||||
},
|
||||
"remote_csv_delay": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "The delay we require on the remote's commitment transaction. If this is\nnot set, it will be scaled automatically with the channel size."
|
||||
},
|
||||
"close_address": {
|
||||
"type": "string",
|
||||
"description": "Close address is an optional address which specifies the address to which\nfunds should be paid out to upon cooperative close. This field may only be\nset if the peer supports the option upfront feature bit (call listpeers\nto check). The remote peer will only accept cooperative closes to this\naddress if it is set.\n\nNote: If this value is set on channel creation, you will *not* be able to\ncooperatively close out to a different address."
|
||||
},
|
||||
"pending_chan_id": {
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"description": "An optional, unique identifier of 32 random bytes that will be used as the\npending channel ID to identify the channel while it is in the pre-pending\nstate."
|
||||
},
|
||||
"commitment_type": {
|
||||
"$ref": "#/definitions/lnrpcCommitmentType",
|
||||
"description": "The explicit commitment type to use. Note this field will only be used if\nthe remote peer supports explicit channel negotiation."
|
||||
}
|
||||
}
|
||||
},
|
||||
"lnrpcBatchOpenChannelRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"channels": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/lnrpcBatchOpenChannel"
|
||||
},
|
||||
"description": "The list of channels to open."
|
||||
},
|
||||
"target_conf": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"description": "The target number of blocks that the funding transaction should be\nconfirmed by."
|
||||
},
|
||||
"sat_per_vbyte": {
|
||||
"type": "string",
|
||||
"format": "int64",
|
||||
"description": "A manual fee rate set in sat/vByte that should be used when crafting the\nfunding transaction."
|
||||
},
|
||||
"min_confs": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"description": "The minimum number of confirmations each one of your outputs used for\nthe funding transaction must satisfy."
|
||||
},
|
||||
"spend_unconfirmed": {
|
||||
"type": "boolean",
|
||||
"description": "Whether unconfirmed outputs should be used as inputs for the funding\ntransaction."
|
||||
},
|
||||
"label": {
|
||||
"type": "string",
|
||||
"description": "An optional label for the batch transaction, limited to 500 characters."
|
||||
}
|
||||
}
|
||||
},
|
||||
"lnrpcBatchOpenChannelResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pending_channels": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/lnrpcPendingUpdate"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"lnrpcChain": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
|
@ -60,6 +60,9 @@ http:
|
|||
- selector: lnrpc.Lightning.OpenChannel
|
||||
post: "/v1/channels/stream"
|
||||
body: "*"
|
||||
- selector: lnrpc.Lightning.BatchOpenChannel
|
||||
post: "/v1/channels/batch"
|
||||
body: "*"
|
||||
- selector: lnrpc.Lightning.FundingStateStep
|
||||
post: "/v1/funding/step"
|
||||
body: "*"
|
||||
|
|
|
@ -143,6 +143,13 @@ type LightningClient interface {
|
|||
//arguments specified in the OpenChannelRequest, this pending channel ID can
|
||||
//then be used to manually progress the channel funding flow.
|
||||
OpenChannel(ctx context.Context, in *OpenChannelRequest, opts ...grpc.CallOption) (Lightning_OpenChannelClient, error)
|
||||
// lncli: `batchopenchannel`
|
||||
//BatchOpenChannel attempts to open multiple single-funded channels in a
|
||||
//single transaction in an atomic way. This means either all channel open
|
||||
//requests succeed at once or all attempts are aborted if any of them fail.
|
||||
//This is the safer variant of using PSBTs to manually fund a batch of
|
||||
//channels through the OpenChannel RPC.
|
||||
BatchOpenChannel(ctx context.Context, in *BatchOpenChannelRequest, opts ...grpc.CallOption) (*BatchOpenChannelResponse, error)
|
||||
//
|
||||
//FundingStateStep is an advanced funding related call that allows the caller
|
||||
//to either execute some preparatory steps for a funding workflow, or
|
||||
|
@ -681,6 +688,15 @@ func (x *lightningOpenChannelClient) Recv() (*OpenStatusUpdate, error) {
|
|||
return m, nil
|
||||
}
|
||||
|
||||
func (c *lightningClient) BatchOpenChannel(ctx context.Context, in *BatchOpenChannelRequest, opts ...grpc.CallOption) (*BatchOpenChannelResponse, error) {
|
||||
out := new(BatchOpenChannelResponse)
|
||||
err := c.cc.Invoke(ctx, "/lnrpc.Lightning/BatchOpenChannel", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *lightningClient) FundingStateStep(ctx context.Context, in *FundingTransitionMsg, opts ...grpc.CallOption) (*FundingStateStepResp, error) {
|
||||
out := new(FundingStateStepResp)
|
||||
err := c.cc.Invoke(ctx, "/lnrpc.Lightning/FundingStateStep", in, out, opts...)
|
||||
|
@ -1294,6 +1310,13 @@ type LightningServer interface {
|
|||
//arguments specified in the OpenChannelRequest, this pending channel ID can
|
||||
//then be used to manually progress the channel funding flow.
|
||||
OpenChannel(*OpenChannelRequest, Lightning_OpenChannelServer) error
|
||||
// lncli: `batchopenchannel`
|
||||
//BatchOpenChannel attempts to open multiple single-funded channels in a
|
||||
//single transaction in an atomic way. This means either all channel open
|
||||
//requests succeed at once or all attempts are aborted if any of them fail.
|
||||
//This is the safer variant of using PSBTs to manually fund a batch of
|
||||
//channels through the OpenChannel RPC.
|
||||
BatchOpenChannel(context.Context, *BatchOpenChannelRequest) (*BatchOpenChannelResponse, error)
|
||||
//
|
||||
//FundingStateStep is an advanced funding related call that allows the caller
|
||||
//to either execute some preparatory steps for a funding workflow, or
|
||||
|
@ -1599,6 +1622,9 @@ func (UnimplementedLightningServer) OpenChannelSync(context.Context, *OpenChanne
|
|||
func (UnimplementedLightningServer) OpenChannel(*OpenChannelRequest, Lightning_OpenChannelServer) error {
|
||||
return status.Errorf(codes.Unimplemented, "method OpenChannel not implemented")
|
||||
}
|
||||
func (UnimplementedLightningServer) BatchOpenChannel(context.Context, *BatchOpenChannelRequest) (*BatchOpenChannelResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method BatchOpenChannel not implemented")
|
||||
}
|
||||
func (UnimplementedLightningServer) FundingStateStep(context.Context, *FundingTransitionMsg) (*FundingStateStepResp, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method FundingStateStep not implemented")
|
||||
}
|
||||
|
@ -2146,6 +2172,24 @@ func (x *lightningOpenChannelServer) Send(m *OpenStatusUpdate) error {
|
|||
return x.ServerStream.SendMsg(m)
|
||||
}
|
||||
|
||||
func _Lightning_BatchOpenChannel_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(BatchOpenChannelRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(LightningServer).BatchOpenChannel(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/lnrpc.Lightning/BatchOpenChannel",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(LightningServer).BatchOpenChannel(ctx, req.(*BatchOpenChannelRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Lightning_FundingStateStep_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(FundingTransitionMsg)
|
||||
if err := dec(in); err != nil {
|
||||
|
@ -2913,6 +2957,10 @@ var Lightning_ServiceDesc = grpc.ServiceDesc{
|
|||
MethodName: "OpenChannelSync",
|
||||
Handler: _Lightning_OpenChannelSync_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "BatchOpenChannel",
|
||||
Handler: _Lightning_BatchOpenChannel_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "FundingStateStep",
|
||||
Handler: _Lightning_FundingStateStep_Handler,
|
||||
|
|
|
@ -11,6 +11,13 @@ import (
|
|||
"github.com/lightningnetwork/lnd/sweep"
|
||||
)
|
||||
|
||||
const (
|
||||
// SubServerName is the name of the sub rpc server. We'll use this name
|
||||
// to register ourselves, and we also require that the main
|
||||
// SubServerConfigDispatcher instance recognize as the name of our
|
||||
SubServerName = "WalletKitRPC"
|
||||
)
|
||||
|
||||
// Config is the primary configuration struct for the WalletKit RPC server. It
|
||||
// contains all the items required for the signer rpc server to carry out its
|
||||
// duties. The fields with struct tags are meant to be parsed as normal
|
||||
|
|
|
@ -2,6 +2,13 @@
|
|||
|
||||
package walletrpc
|
||||
|
||||
const (
|
||||
// SubServerName is the name of the sub rpc server. We'll use this name
|
||||
// to register ourselves, and we also require that the main
|
||||
// SubServerConfigDispatcher instance recognize as the name of our
|
||||
SubServerName = "WalletKitRPC"
|
||||
)
|
||||
|
||||
// Config is the primary configuration struct for the WalletKit RPC server.
|
||||
// When the server isn't active (via the build flag), callers outside this
|
||||
// package will see this shell of a config file.
|
||||
|
|
|
@ -16,13 +16,13 @@ func createNewSubServer(configRegistry lnrpc.SubServerConfigDispatcher) (
|
|||
*WalletKit, lnrpc.MacaroonPerms, error) {
|
||||
|
||||
// We'll attempt to look up the config that we expect, according to our
|
||||
// subServerName name. If we can't find this, then we'll exit with an
|
||||
// SubServerName name. If we can't find this, then we'll exit with an
|
||||
// error, as we're unable to properly initialize ourselves without this
|
||||
// config.
|
||||
walletKitServerConf, ok := configRegistry.FetchConfig(subServerName)
|
||||
walletKitServerConf, ok := configRegistry.FetchConfig(SubServerName)
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("unable to find config for "+
|
||||
"subserver type %s", subServerName)
|
||||
"subserver type %s", SubServerName)
|
||||
}
|
||||
|
||||
// Now that we've found an object mapping to our service name, we'll
|
||||
|
@ -30,7 +30,7 @@ func createNewSubServer(configRegistry lnrpc.SubServerConfigDispatcher) (
|
|||
config, ok := walletKitServerConf.(*Config)
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("wrong type of config for "+
|
||||
"subserver %s, expected %T got %T", subServerName,
|
||||
"subserver %s, expected %T got %T", SubServerName,
|
||||
&Config{}, walletKitServerConf)
|
||||
}
|
||||
|
||||
|
@ -68,7 +68,7 @@ func createNewSubServer(configRegistry lnrpc.SubServerConfigDispatcher) (
|
|||
|
||||
func init() {
|
||||
subServer := &lnrpc.SubServerDriver{
|
||||
SubServerName: subServerName,
|
||||
SubServerName: SubServerName,
|
||||
NewGrpcHandler: func() lnrpc.GrpcHandler {
|
||||
return &ServerShell{}
|
||||
},
|
||||
|
@ -78,6 +78,6 @@ func init() {
|
|||
// sub-RPC server within the global lnrpc package namespace.
|
||||
if err := lnrpc.RegisterSubServer(subServer); err != nil {
|
||||
panic(fmt.Sprintf("failed to register sub server driver '%s': %v",
|
||||
subServerName, err))
|
||||
SubServerName, err))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,13 +37,6 @@ import (
|
|||
"gopkg.in/macaroon-bakery.v2/bakery"
|
||||
)
|
||||
|
||||
const (
|
||||
// subServerName is the name of the sub rpc server. We'll use this name
|
||||
// to register ourselves, and we also require that the main
|
||||
// SubServerConfigDispatcher instance recognize as the name of our
|
||||
subServerName = "WalletKitRPC"
|
||||
)
|
||||
|
||||
var (
|
||||
// macaroonOps are the set of capabilities that our minted macaroon (if
|
||||
// it doesn't already exist) will have.
|
||||
|
@ -256,7 +249,7 @@ func (w *WalletKit) Stop() error {
|
|||
//
|
||||
// NOTE: This is part of the lnrpc.SubServer interface.
|
||||
func (w *WalletKit) Name() string {
|
||||
return subServerName
|
||||
return SubServerName
|
||||
}
|
||||
|
||||
// RegisterWithRootServer will be called by the root gRPC server to direct a
|
||||
|
|
|
@ -26,7 +26,6 @@ import (
|
|||
// conditions. Finally, the chain itself is checked to ensure the closing
|
||||
// transaction was mined.
|
||||
func testBasicChannelFunding(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
|
||||
// Run through the test with combinations of all the different
|
||||
// commitment types.
|
||||
allTypes := []lnrpc.CommitmentType{
|
||||
|
@ -366,7 +365,7 @@ func testUnconfirmedChannelFunding(net *lntest.NetworkHarness, t *harnessTest) {
|
|||
closeChannelAndAssert(t, net, carol, chanPoint, false)
|
||||
}
|
||||
|
||||
// testexternalfundingchanpoint tests that we're able to carry out a normal
|
||||
// testExternalFundingChanPoint tests that we're able to carry out a normal
|
||||
// channel funding workflow given a channel point that was constructed outside
|
||||
// the main daemon.
|
||||
func testExternalFundingChanPoint(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
|
@ -744,3 +743,122 @@ func deriveFundingShim(net *lntest.NetworkHarness, t *harnessTest,
|
|||
|
||||
return fundingShim, chanPoint, txid
|
||||
}
|
||||
|
||||
// testBatchChanFunding makes sure multiple channels can be opened in one batch
|
||||
// transaction in an atomic way.
|
||||
func testBatchChanFunding(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
ctxb := context.Background()
|
||||
|
||||
// First, we'll create two new nodes that we'll use to open channels
|
||||
// to during this test. Carol has a high minimum funding amount that
|
||||
// we'll use to trigger an error during the batch channel open.
|
||||
carol := net.NewNode(t.t, "carol", []string{"--minchansize=200000"})
|
||||
defer shutdownAndAssert(net, t, carol)
|
||||
|
||||
dave := net.NewNode(t.t, "dave", nil)
|
||||
defer shutdownAndAssert(net, t, dave)
|
||||
|
||||
// Before we start the test, we'll ensure Alice is connected to Carol
|
||||
// and Dave so she can open channels to both of them (and Bob).
|
||||
net.EnsureConnected(t.t, net.Alice, net.Bob)
|
||||
net.EnsureConnected(t.t, net.Alice, carol)
|
||||
net.EnsureConnected(t.t, net.Alice, dave)
|
||||
|
||||
// Let's create our batch TX request. This first one should fail as we
|
||||
// open a channel to Carol that is too small for her min chan size.
|
||||
batchReq := &lnrpc.BatchOpenChannelRequest{
|
||||
SatPerVbyte: 12,
|
||||
MinConfs: 1,
|
||||
Channels: []*lnrpc.BatchOpenChannel{{
|
||||
NodePubkey: net.Bob.PubKey[:],
|
||||
LocalFundingAmount: 100_000,
|
||||
}, {
|
||||
NodePubkey: carol.PubKey[:],
|
||||
LocalFundingAmount: 100_000,
|
||||
}, {
|
||||
NodePubkey: dave.PubKey[:],
|
||||
LocalFundingAmount: 100_000,
|
||||
}},
|
||||
}
|
||||
|
||||
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
_, err := net.Alice.BatchOpenChannel(ctxt, batchReq)
|
||||
require.Error(t.t, err)
|
||||
require.Contains(t.t, err.Error(), "initial negotiation failed")
|
||||
|
||||
// Let's fix the minimum amount for Carol now and try again.
|
||||
batchReq.Channels[1].LocalFundingAmount = 200_000
|
||||
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
batchResp, err := net.Alice.BatchOpenChannel(ctxt, batchReq)
|
||||
require.NoError(t.t, err)
|
||||
require.Len(t.t, batchResp.PendingChannels, 3)
|
||||
|
||||
txHash, err := chainhash.NewHash(batchResp.PendingChannels[0].Txid)
|
||||
require.NoError(t.t, err)
|
||||
|
||||
chanPoint1 := &lnrpc.ChannelPoint{
|
||||
FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{
|
||||
FundingTxidBytes: batchResp.PendingChannels[0].Txid,
|
||||
},
|
||||
OutputIndex: batchResp.PendingChannels[0].OutputIndex,
|
||||
}
|
||||
chanPoint2 := &lnrpc.ChannelPoint{
|
||||
FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{
|
||||
FundingTxidBytes: batchResp.PendingChannels[1].Txid,
|
||||
},
|
||||
OutputIndex: batchResp.PendingChannels[1].OutputIndex,
|
||||
}
|
||||
chanPoint3 := &lnrpc.ChannelPoint{
|
||||
FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{
|
||||
FundingTxidBytes: batchResp.PendingChannels[2].Txid,
|
||||
},
|
||||
OutputIndex: batchResp.PendingChannels[2].OutputIndex,
|
||||
}
|
||||
|
||||
block := mineBlocks(t, net, 6, 1)[0]
|
||||
assertTxInBlock(t, block, txHash)
|
||||
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
err = net.Alice.WaitForNetworkChannelOpen(ctxt, chanPoint1)
|
||||
require.NoError(t.t, err)
|
||||
err = net.Alice.WaitForNetworkChannelOpen(ctxt, chanPoint2)
|
||||
require.NoError(t.t, err)
|
||||
err = net.Alice.WaitForNetworkChannelOpen(ctxt, chanPoint3)
|
||||
require.NoError(t.t, err)
|
||||
|
||||
// With the channel open, ensure that it is counted towards Carol's
|
||||
// total channel balance.
|
||||
balReq := &lnrpc.ChannelBalanceRequest{}
|
||||
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
balRes, err := net.Alice.ChannelBalance(ctxt, balReq)
|
||||
require.NoError(t.t, err)
|
||||
require.NotEqual(t.t, int64(0), balRes.LocalBalance.Sat)
|
||||
|
||||
// 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, cancel = context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
resp, err := carol.AddInvoice(ctxt, invoice)
|
||||
require.NoError(t.t, err)
|
||||
err = completePaymentRequests(
|
||||
net.Alice, net.Alice.RouterClient,
|
||||
[]string{resp.PaymentRequest}, true,
|
||||
)
|
||||
require.NoError(t.t, err)
|
||||
|
||||
// To conclude, we'll close the newly created channel between Carol and
|
||||
// Dave. This function will also block until the channel is closed and
|
||||
// will additionally assert the relevant channel closing post
|
||||
// conditions.
|
||||
closeChannelAndAssert(t, net, net.Alice, chanPoint1, false)
|
||||
closeChannelAndAssert(t, net, net.Alice, chanPoint2, false)
|
||||
closeChannelAndAssert(t, net, net.Alice, chanPoint3, false)
|
||||
}
|
||||
|
|
|
@ -274,6 +274,10 @@ var allTestCases = []*testCase{
|
|||
name: "psbt channel funding",
|
||||
test: testPsbtChanFunding,
|
||||
},
|
||||
{
|
||||
name: "batch channel funding",
|
||||
test: testBatchChanFunding,
|
||||
},
|
||||
{
|
||||
name: "sendtoroute multi path payment",
|
||||
test: testSendToRouteMultiPath,
|
||||
|
|
|
@ -70,6 +70,8 @@
|
|||
<time> [ERR] FNDG: received funding error from <hex>: chan_id=<hex>, err=chan size of 10.00000001 BTC exceeds maximum chan size of 10 BTC
|
||||
<time> [ERR] FNDG: received funding error from <hex>: chan_id=<hex>, err=Number of pending channels exceed maximum
|
||||
<time> [ERR] FNDG: received funding error from <hex>: chan_id=<hex>, err=Synchronizing blockchain
|
||||
<time> [ERR] FNDG: received funding error from <hex>: chan_id=<hex>, err=chan size of 0.001 BTC is below min chan size of 0.002 BTC
|
||||
<time> [ERR] FNDG: received funding error from <hex>: chan_id=<hex>, err=funding failed due to internal error
|
||||
<time> [ERR] FNDG: Unable to add new channel <chan_point> with peer <hex>: canceled adding new channel
|
||||
<time> [ERR] FNDG: Unable to add new channel <chan_point> with peer <hex>: peer exiting
|
||||
<time> [ERR] FNDG: Unable to add new channel <chan_point> with peer <hex>: unable to get best block: the client has been shutdown
|
||||
|
@ -92,6 +94,7 @@
|
|||
<time> [ERR] FNDG: Unable to advance state(<chan_point>): failed sending fundingLocked: funding manager shutting down
|
||||
<time> [ERR] FNDG: Unable to advance state(<chan_point>): funding manager shutting down
|
||||
<time> [ERR] FNDG: unable to cancel reservation: no active reservations for peer(<hex>)
|
||||
<time> [ERR] FNDG: Unable to handle funding accept message for peer_key=<hex>, pending_chan_id=<hex>: aborting PSBT flow: user canceled funding
|
||||
<time> [ERR] FNDG: unable to report short chan id: link <hex> not found
|
||||
<time> [ERR] FNDG: Unable to send channel proof: channel announcement proof for short_chan_id=<cid> isn't valid: can't verify first bitcoin signature
|
||||
<time> [ERR] FNDG: Unable to send channel proof: gossiper is shutting down
|
||||
|
@ -99,6 +102,7 @@
|
|||
<time> [ERR] FNDG: Unable to send channel proof: unable add proof to the channel chanID=<hex>: edge not found
|
||||
<time> [ERR] FNDG: Unable to send node announcement: gossiper is shutting down
|
||||
<time> [ERR] FNDG: Unable to send node announcement: router shutting down
|
||||
<time> [ERR] FNDG: unable to open channel to NodeKey(<hex>): remote canceled funding, possibly timed out: received funding error from <hex>: chan_id=<hex>, err=chan size of 0.001 BTC is below min chan size of 0.002 BTC
|
||||
<time> [ERR] HSWC: AmountBelowMinimum(amt=<amt>, update=(lnwire.ChannelUpdate) {
|
||||
<time> [ERR] HSWC: ChannelLink(<chan_point>): failing link: ChannelPoint(<chan_point>): received error from peer: chan_id=<hex>, err=internal error with error: remote error
|
||||
<time> [ERR] HSWC: ChannelLink(<chan_point>): failing link: ChannelPoint(<chan_point>): received error from peer: chan_id=<hex>, err=invalid update with error: remote error
|
||||
|
@ -188,6 +192,7 @@
|
|||
<time> [ERR] RPCS: Failed sending response: rpc error: code = Canceled desc = context canceled
|
||||
<time> [ERR] RPCS: Failed sending response: rpc error: code = Internal desc = transport: transport: the stream is done or WriteHeader was already called
|
||||
<time> [ERR] RPCS: [/invoicesrpc.Invoices/SubscribeSingleInvoice]: rpc error: code = Canceled desc = context canceled
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/BatchOpenChannel]: batch funding failed: error batch opening channel, initial negotiation failed: remote canceled funding, possibly timed out: received funding error from <hex>: chan_id=<hex>, err=chan size of 0.001 BTC is below min chan size of 0.002 BTC
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/BakeMacaroon]: invalid permission action. supported actions are [read write generate], supported entities are [onchain offchain address message peers info invoices signer macaroon uri]
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/BakeMacaroon]: invalid permission entity. supported actions are [read write generate], supported entities are [onchain offchain address message peers info invoices signer macaroon uri]
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/BakeMacaroon]: permission list cannot be empty. specify at least one action/entity pair. supported actions are [read write generate], supported entities are [onchain offchain address message peers info invoices signer macaroon uri]
|
||||
|
|
156
rpcserver.go
156
rpcserver.go
|
@ -52,6 +52,7 @@ import (
|
|||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
|
||||
|
@ -300,6 +301,13 @@ func MainRPCServerPermissions() map[string][]bakery.Op {
|
|||
Entity: "offchain",
|
||||
Action: "write",
|
||||
}},
|
||||
"/lnrpc.Lightning/BatchOpenChannel": {{
|
||||
Entity: "onchain",
|
||||
Action: "write",
|
||||
}, {
|
||||
Entity: "offchain",
|
||||
Action: "write",
|
||||
}},
|
||||
"/lnrpc.Lightning/OpenChannelSync": {{
|
||||
Entity: "onchain",
|
||||
Action: "write",
|
||||
|
@ -1788,7 +1796,7 @@ func (r *rpcServer) canOpenChannel() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// praseOpenChannelReq parses an OpenChannelRequest message into an InitFundingMsg
|
||||
// parseOpenChannelReq parses an OpenChannelRequest message into an InitFundingMsg
|
||||
// struct. The logic is abstracted so that it can be shared between OpenChannel
|
||||
// and OpenChannelSync.
|
||||
func (r *rpcServer) parseOpenChannelReq(in *lnrpc.OpenChannelRequest,
|
||||
|
@ -2107,6 +2115,84 @@ func (r *rpcServer) OpenChannelSync(ctx context.Context,
|
|||
}
|
||||
}
|
||||
|
||||
// BatchOpenChannel attempts to open multiple single-funded channels in a
|
||||
// single transaction in an atomic way. This means either all channel open
|
||||
// requests succeed at once or all attempts are aborted if any of them fail.
|
||||
// This is the safer variant of using PSBTs to manually fund a batch of
|
||||
// channels through the OpenChannel RPC.
|
||||
func (r *rpcServer) BatchOpenChannel(ctx context.Context,
|
||||
in *lnrpc.BatchOpenChannelRequest) (*lnrpc.BatchOpenChannelResponse,
|
||||
error) {
|
||||
|
||||
if err := r.canOpenChannel(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We need the wallet kit server to do the heavy lifting on the PSBT
|
||||
// part. If we didn't rely on re-using the wallet kit server's logic we
|
||||
// would need to re-implement everything here. Since we deliver lnd with
|
||||
// the wallet kit server enabled by default we can assume it's okay to
|
||||
// make this functionality dependent on that server being active.
|
||||
var walletKitServer walletrpc.WalletKitServer
|
||||
for _, subServer := range r.subServers {
|
||||
if subServer.Name() == walletrpc.SubServerName {
|
||||
walletKitServer = subServer.(walletrpc.WalletKitServer)
|
||||
}
|
||||
}
|
||||
if walletKitServer == nil {
|
||||
return nil, fmt.Errorf("batch channel open is only possible " +
|
||||
"if walletrpc subserver is active")
|
||||
}
|
||||
|
||||
rpcsLog.Debugf("[batchopenchannel] request to open batch of %d "+
|
||||
"channels", len(in.Channels))
|
||||
|
||||
// Make sure there is at least one channel to open. We could say we want
|
||||
// at least two channels for a batch. But maybe it's nice if developers
|
||||
// can use the same API for a single channel as well as a batch of
|
||||
// channels.
|
||||
if len(in.Channels) == 0 {
|
||||
return nil, fmt.Errorf("specify at least one channel")
|
||||
}
|
||||
|
||||
// In case we remove a pending channel from the database, we need to set
|
||||
// a close height, so we'll just use the current best known height.
|
||||
_, bestHeight, err := r.server.cc.ChainIO.GetBestBlock()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error fetching best block: %v", err)
|
||||
}
|
||||
|
||||
// So far everything looks good and we can now start the heavy lifting
|
||||
// that's done in the funding package.
|
||||
requestParser := func(req *lnrpc.OpenChannelRequest) (
|
||||
*funding.InitFundingMsg, error) {
|
||||
|
||||
return r.parseOpenChannelReq(req, false)
|
||||
}
|
||||
channelAbandoner := func(point *wire.OutPoint) error {
|
||||
return r.abandonChan(point, uint32(bestHeight))
|
||||
}
|
||||
batcher := funding.NewBatcher(&funding.BatchConfig{
|
||||
RequestParser: requestParser,
|
||||
ChannelAbandoner: channelAbandoner,
|
||||
ChannelOpener: r.server.OpenChannel,
|
||||
WalletKitServer: walletKitServer,
|
||||
Wallet: r.server.cc.Wallet,
|
||||
NetParams: &r.server.cc.Wallet.Cfg.NetParams,
|
||||
Quit: r.quit,
|
||||
})
|
||||
rpcPoints, err := batcher.BatchFund(ctx, in)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("batch funding failed: %v", err)
|
||||
}
|
||||
|
||||
// Now all that's left to do is send back the response with the channel
|
||||
// points we created.
|
||||
return &lnrpc.BatchOpenChannelResponse{
|
||||
PendingChannels: rpcPoints,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CloseChannel attempts to close an active channel identified by its channel
|
||||
// point. The actions of this method can additionally be augmented to attempt
|
||||
// a force close after a timeout period in the case of an inactive peer.
|
||||
|
@ -2394,6 +2480,44 @@ func abandonChanFromGraph(chanGraph *channeldb.ChannelGraph,
|
|||
return chanGraph.DeleteChannelEdges(false, chanID)
|
||||
}
|
||||
|
||||
// abandonChan removes a channel from the database, graph and contract court.
|
||||
func (r *rpcServer) abandonChan(chanPoint *wire.OutPoint,
|
||||
bestHeight uint32) error {
|
||||
|
||||
// Abandoning a channel is a three-step process: remove from the open
|
||||
// channel state, remove from the graph, remove from the contract
|
||||
// court. Between any step it's possible that the users restarts the
|
||||
// process all over again. As a result, each of the steps below are
|
||||
// intended to be idempotent.
|
||||
err := r.server.chanStateDB.AbandonChannel(chanPoint, bestHeight)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = abandonChanFromGraph(r.server.graphDB, chanPoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = r.server.chainArb.ResolveContract(*chanPoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If this channel was in the process of being closed, but didn't fully
|
||||
// close, then it's possible that the nursery is hanging on to some
|
||||
// state. To err on the side of caution, we'll now attempt to wipe any
|
||||
// state for this channel from the nursery.
|
||||
err = r.server.utxoNursery.cfg.Store.RemoveChannel(chanPoint)
|
||||
if err != nil && err != ErrContractNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
// Finally, notify the backup listeners that the channel can be removed
|
||||
// from any channel backups.
|
||||
r.server.channelNotifier.NotifyClosedChannelEvent(*chanPoint)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
@ -2472,36 +2596,10 @@ func (r *rpcServer) AbandonChannel(_ context.Context,
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// Abandoning a channel is a three step process: remove from the open
|
||||
// channel state, remove from the graph, remove from the contract
|
||||
// court. Between any step it's possible that the users restarts the
|
||||
// process all over again. As a result, each of the steps below are
|
||||
// intended to be idempotent.
|
||||
err = r.server.chanStateDB.AbandonChannel(chanPoint, uint32(bestHeight))
|
||||
if err != nil {
|
||||
// Remove the channel from the graph, database and contract court.
|
||||
if err := r.abandonChan(chanPoint, uint32(bestHeight)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = abandonChanFromGraph(r.server.graphDB, chanPoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = r.server.chainArb.ResolveContract(*chanPoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If this channel was in the process of being closed, but didn't fully
|
||||
// close, then it's possible that the nursery is hanging on to some
|
||||
// state. To err on the side of caution, we'll now attempt to wipe any
|
||||
// state for this channel from the nursery.
|
||||
err = r.server.utxoNursery.cfg.Store.RemoveChannel(chanPoint)
|
||||
if err != nil && err != ErrContractNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Finally, notify the backup listeners that the channel can be removed
|
||||
// from any channel backups.
|
||||
r.server.channelNotifier.NotifyClosedChannelEvent(*chanPoint)
|
||||
|
||||
return &lnrpc.AbandonChannelResponse{}, nil
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue