Merge pull request #7631 from hieblmi/sendoutputs-reserve
rpc: anchor reserve check for `SendOutputs`
This commit is contained in:
5 files changed
+172
-13
No files matched your search
@@ -13,6 +13,11 @@
|
|||||||
the entire retribution struct. This reduces the amount of data that needs to
|
the entire retribution struct. This reduces the amount of data that needs to
|
||||||
be held in memory.
|
be held in memory.
|
||||||
|
|
||||||
|
## RPC
|
||||||
|
|
||||||
|
* [SendOutputs](https://github.com/lightningnetwork/lnd/pull/7631) now adheres
|
||||||
|
to the anchor channel reserve requirement.
|
||||||
|
|
||||||
## Misc
|
## Misc
|
||||||
|
|
||||||
* [Generate default macaroons
|
* [Generate default macaroons
|
||||||
@@ -23,4 +28,5 @@ unlock or create.
|
|||||||
|
|
||||||
* Daniel McNally
|
* Daniel McNally
|
||||||
* Elle Mouton
|
* Elle Mouton
|
||||||
|
* hieblmi
|
||||||
* Jordi Montes
|
* Jordi Montes
|
||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
"github.com/lightningnetwork/lnd/lnrpc"
|
"github.com/lightningnetwork/lnd/lnrpc"
|
||||||
"github.com/lightningnetwork/lnd/lnrpc/chainrpc"
|
"github.com/lightningnetwork/lnd/lnrpc/chainrpc"
|
||||||
|
"github.com/lightningnetwork/lnd/lnrpc/signrpc"
|
||||||
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
||||||
"github.com/lightningnetwork/lnd/lntest"
|
"github.com/lightningnetwork/lnd/lntest"
|
||||||
"github.com/lightningnetwork/lnd/lntest/node"
|
"github.com/lightningnetwork/lnd/lntest/node"
|
||||||
@@ -26,6 +27,7 @@ func testChainKit(ht *lntest.HarnessTest) {
|
|||||||
// avoid the need to start separate nodes.
|
// avoid the need to start separate nodes.
|
||||||
testChainKitGetBlock(ht)
|
testChainKitGetBlock(ht)
|
||||||
testChainKitGetBlockHash(ht)
|
testChainKitGetBlockHash(ht)
|
||||||
|
testChainKitSendOutputsAnchorReserve(ht)
|
||||||
}
|
}
|
||||||
|
|
||||||
// testChainKitGetBlock ensures that given a block hash, the RPC endpoint
|
// testChainKitGetBlock ensures that given a block hash, the RPC endpoint
|
||||||
@@ -74,6 +76,96 @@ func testChainKitGetBlockHash(ht *lntest.HarnessTest) {
|
|||||||
require.Equal(ht, expected, actual)
|
require.Equal(ht, expected, actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// testChainKitSendOutputsAnchorReserve checks if the SendOutputs rpc prevents
|
||||||
|
// our wallet balance to drop below the required anchor channel reserve amount.
|
||||||
|
func testChainKitSendOutputsAnchorReserve(ht *lntest.HarnessTest) {
|
||||||
|
// Start two nodes supporting anchor channels.
|
||||||
|
args := lntest.NodeArgsForCommitType(lnrpc.CommitmentType_ANCHORS)
|
||||||
|
|
||||||
|
// NOTE: we cannot reuse the standby node here as the test requires the
|
||||||
|
// node to start with no UTXOs.
|
||||||
|
charlie := ht.NewNode("Charlie", args)
|
||||||
|
bob := ht.Bob
|
||||||
|
ht.RestartNodeWithExtraArgs(bob, args)
|
||||||
|
|
||||||
|
// We'll start the test by sending Charlie some coins.
|
||||||
|
fundingAmount := btcutil.Amount(100_000)
|
||||||
|
ht.FundCoins(fundingAmount, charlie)
|
||||||
|
|
||||||
|
// Before opening the channel we ensure that the nodes are connected.
|
||||||
|
ht.EnsureConnected(charlie, bob)
|
||||||
|
|
||||||
|
// We'll get the anchor reserve that is required for a single channel.
|
||||||
|
reserve := charlie.RPC.RequiredReserve(
|
||||||
|
&walletrpc.RequiredReserveRequest{
|
||||||
|
AdditionalPublicChannels: 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Charlie opens an anchor channel and keeps twice the amount of the
|
||||||
|
// anchor reserve in her wallet.
|
||||||
|
chanAmt := fundingAmount - 2*btcutil.Amount(reserve.RequiredReserve)
|
||||||
|
outpoint := ht.OpenChannel(charlie, bob, lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
CommitmentType: lnrpc.CommitmentType_ANCHORS,
|
||||||
|
SatPerVByte: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Now we obtain a taproot address from bob which Charlie will use to
|
||||||
|
// send coins to him via the SendOutputs rpc.
|
||||||
|
address := bob.RPC.NewAddress(&lnrpc.NewAddressRequest{
|
||||||
|
Type: lnrpc.AddressType_TAPROOT_PUBKEY,
|
||||||
|
})
|
||||||
|
decodedAddr := ht.DecodeAddress(address.Address)
|
||||||
|
addrScript := ht.PayToAddrScript(decodedAddr)
|
||||||
|
|
||||||
|
// First she will try to send Bob an amount that would undershoot her
|
||||||
|
// reserve requirement by one satoshi.
|
||||||
|
balance := charlie.RPC.WalletBalance()
|
||||||
|
utxo := &wire.TxOut{
|
||||||
|
Value: balance.TotalBalance - reserve.RequiredReserve + 1,
|
||||||
|
PkScript: addrScript,
|
||||||
|
}
|
||||||
|
req := &walletrpc.SendOutputsRequest{
|
||||||
|
Outputs: []*signrpc.TxOut{{
|
||||||
|
Value: utxo.Value,
|
||||||
|
PkScript: utxo.PkScript,
|
||||||
|
}},
|
||||||
|
SatPerKw: 2400,
|
||||||
|
MinConfs: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
// We try to send the reserve violating transaction and expect it to
|
||||||
|
// fail.
|
||||||
|
_, err := charlie.RPC.WalletKit.SendOutputs(ht.Context(), req)
|
||||||
|
require.ErrorContains(ht, err, walletrpc.ErrInsufficientReserve.Error())
|
||||||
|
|
||||||
|
ht.MineBlocksAndAssertNumTxes(1, 0)
|
||||||
|
|
||||||
|
// Next she will try to send Bob an amount that just leaves enough
|
||||||
|
// reserves in her wallet.
|
||||||
|
utxo = &wire.TxOut{
|
||||||
|
Value: balance.TotalBalance - reserve.RequiredReserve,
|
||||||
|
PkScript: addrScript,
|
||||||
|
}
|
||||||
|
req = &walletrpc.SendOutputsRequest{
|
||||||
|
Outputs: []*signrpc.TxOut{{
|
||||||
|
Value: utxo.Value,
|
||||||
|
PkScript: utxo.PkScript,
|
||||||
|
}},
|
||||||
|
SatPerKw: 2400,
|
||||||
|
MinConfs: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
// This second transaction should be published correctly.
|
||||||
|
charlie.RPC.SendOutputs(req)
|
||||||
|
|
||||||
|
ht.MineBlocksAndAssertNumTxes(1, 1)
|
||||||
|
|
||||||
|
// Clean up our test setup.
|
||||||
|
ht.CloseChannel(charlie, outpoint)
|
||||||
|
}
|
||||||
|
|
||||||
// testCPFP ensures that the daemon can bump an unconfirmed transaction's fee
|
// testCPFP ensures that the daemon can bump an unconfirmed transaction's fee
|
||||||
// rate by broadcasting a Child-Pays-For-Parent (CPFP) transaction.
|
// rate by broadcasting a Child-Pays-For-Parent (CPFP) transaction.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package walletrpc
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
|
||||||
|
// ErrZeroLabel is returned when an attempt is made to label a
|
||||||
|
// transaction with an empty label.
|
||||||
|
ErrZeroLabel = errors.New("cannot label transaction with empty " +
|
||||||
|
"label")
|
||||||
|
|
||||||
|
// ErrInsufficientReserve is returned when SendOutputs wouldn't leave
|
||||||
|
// enough funds in the wallet to cover for the anchor reserve.
|
||||||
|
ErrInsufficientReserve = errors.New("the outputs to be sent " +
|
||||||
|
"would leave insufficient reserves for anchor channels in " +
|
||||||
|
"the wallet")
|
||||||
|
)
|
||||||
@@ -222,10 +222,6 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrZeroLabel is returned when an attempt is made to label a transaction with
|
|
||||||
// an empty label.
|
|
||||||
var ErrZeroLabel = errors.New("cannot label transaction with empty label")
|
|
||||||
|
|
||||||
// ServerShell is a shell struct holding a reference to the actual sub-server.
|
// ServerShell is a shell struct holding a reference to the actual sub-server.
|
||||||
// It is used to register the gRPC sub-server with the root server before we
|
// It is used to register the gRPC sub-server with the root server before we
|
||||||
// have the necessary dependencies to populate the actual sub-server.
|
// have the necessary dependencies to populate the actual sub-server.
|
||||||
@@ -394,8 +390,8 @@ func (w *WalletKit) ListUnspent(ctx context.Context,
|
|||||||
// Force min_confs and max_confs to be zero if unconfirmed_only is
|
// Force min_confs and max_confs to be zero if unconfirmed_only is
|
||||||
// true.
|
// true.
|
||||||
if req.UnconfirmedOnly && (req.MinConfs != 0 || req.MaxConfs != 0) {
|
if req.UnconfirmedOnly && (req.MinConfs != 0 || req.MaxConfs != 0) {
|
||||||
return nil, fmt.Errorf("min_confs and max_confs must be zero if " +
|
return nil, fmt.Errorf("min_confs and max_confs must be zero " +
|
||||||
"unconfirmed_only is true")
|
"if unconfirmed_only is true")
|
||||||
}
|
}
|
||||||
|
|
||||||
// When unconfirmed_only is inactive and max_confs is zero (default
|
// When unconfirmed_only is inactive and max_confs is zero (default
|
||||||
@@ -666,30 +662,58 @@ func (w *WalletKit) SendOutputs(ctx context.Context,
|
|||||||
// Before we can request this transaction to be created, we'll need to
|
// Before we can request this transaction to be created, we'll need to
|
||||||
// amp the protos back into the format that the internal wallet will
|
// amp the protos back into the format that the internal wallet will
|
||||||
// recognize.
|
// recognize.
|
||||||
|
var totalOutputValue int64
|
||||||
outputsToCreate := make([]*wire.TxOut, 0, len(req.Outputs))
|
outputsToCreate := make([]*wire.TxOut, 0, len(req.Outputs))
|
||||||
for _, output := range req.Outputs {
|
for _, output := range req.Outputs {
|
||||||
outputsToCreate = append(outputsToCreate, &wire.TxOut{
|
outputsToCreate = append(outputsToCreate, &wire.TxOut{
|
||||||
Value: output.Value,
|
Value: output.Value,
|
||||||
PkScript: output.PkScript,
|
PkScript: output.PkScript,
|
||||||
})
|
})
|
||||||
|
totalOutputValue += output.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then, we'll extract the minimum number of confirmations that each
|
// Then, we'll extract the minimum number of confirmations that each
|
||||||
// output we use to fund the transaction should satisfy.
|
// output we use to fund the transaction should satisfy.
|
||||||
minConfs, err := lnrpc.ExtractMinConfs(req.MinConfs, req.SpendUnconfirmed)
|
minConfs, err := lnrpc.ExtractMinConfs(
|
||||||
|
req.MinConfs, req.SpendUnconfirmed,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Before sending out funds we need to ensure that the remainder of our
|
||||||
|
// wallet funds would cover for the anchor reserve requirement. We'll
|
||||||
|
// also take unconfirmed funds into account.
|
||||||
|
walletBalance, err := w.cfg.Wallet.ConfirmedBalance(
|
||||||
|
0, lnwallet.DefaultAccountName,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll get the currently required reserve amount.
|
||||||
|
reserve, err := w.RequiredReserve(ctx, &RequiredReserveRequest{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then we check if our current wallet balance undershoots the required
|
||||||
|
// reserve if we'd send out the outputs specified in the request.
|
||||||
|
if int64(walletBalance)-totalOutputValue < reserve.RequiredReserve {
|
||||||
|
return nil, ErrInsufficientReserve
|
||||||
|
}
|
||||||
|
|
||||||
label, err := labels.ValidateAPI(req.Label)
|
label, err := labels.ValidateAPI(req.Label)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now that we have the outputs mapped, we can request that the wallet
|
// Now that we have the outputs mapped and checked for the reserve
|
||||||
// attempt to create this transaction.
|
// requirement, we can request that the wallet attempts to create this
|
||||||
|
// transaction.
|
||||||
tx, err := w.cfg.Wallet.SendOutputs(
|
tx, err := w.cfg.Wallet.SendOutputs(
|
||||||
outputsToCreate, chainfee.SatPerKWeight(req.SatPerKw), minConfs, label,
|
outputsToCreate, chainfee.SatPerKWeight(req.SatPerKw), minConfs,
|
||||||
|
label,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -923,7 +947,10 @@ func (w *WalletKit) BumpFee(ctx context.Context,
|
|||||||
err)
|
err)
|
||||||
}
|
}
|
||||||
|
|
||||||
inp := input.NewBaseInput(op, witnessType, signDesc, uint32(currentHeight))
|
inp := input.NewBaseInput(
|
||||||
|
op, witnessType, signDesc, uint32(currentHeight),
|
||||||
|
)
|
||||||
|
|
||||||
sweepParams := sweep.Params{Fee: feePreference}
|
sweepParams := sweep.Params{Fee: feePreference}
|
||||||
if _, err = w.cfg.Sweeper.SweepInput(inp, sweepParams); err != nil {
|
if _, err = w.cfg.Sweeper.SweepInput(inp, sweepParams); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1524,7 +1551,8 @@ func (w *WalletKit) ListAccounts(ctx context.Context,
|
|||||||
keyScopeFilter = &keyScope
|
keyScopeFilter = &keyScope
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unhandled address type %v", req.AddressType)
|
return nil, fmt.Errorf("unhandled address type %v",
|
||||||
|
req.AddressType)
|
||||||
}
|
}
|
||||||
|
|
||||||
accounts, err := w.cfg.Wallet.ListAccounts(req.Name, keyScopeFilter)
|
accounts, err := w.cfg.Wallet.ListAccounts(req.Name, keyScopeFilter)
|
||||||
@@ -1638,7 +1666,8 @@ func parseAddrType(addrType AddressType,
|
|||||||
switch addrType {
|
switch addrType {
|
||||||
case AddressType_UNKNOWN:
|
case AddressType_UNKNOWN:
|
||||||
if required {
|
if required {
|
||||||
return nil, errors.New("an address type must be specified")
|
return nil, fmt.Errorf("an address type must be " +
|
||||||
|
"specified")
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
|
||||||
|
|||||||
@@ -290,3 +290,18 @@ func (h *HarnessRPC) ImportTapscript(
|
|||||||
|
|
||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RequiredReserve makes a RPC call to the node's WalletKitClient and asserts.
|
||||||
|
//
|
||||||
|
//nolint:lll
|
||||||
|
func (h *HarnessRPC) RequiredReserve(
|
||||||
|
req *walletrpc.RequiredReserveRequest) *walletrpc.RequiredReserveResponse {
|
||||||
|
|
||||||
|
ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
resp, err := h.WalletKit.RequiredReserve(ctxt, req)
|
||||||
|
h.NoError(err, "RequiredReserve")
|
||||||
|
|
||||||
|
return resp
|
||||||
|
}
|
||||||
Reference in new issue
Block a user