From a3addcc9275c55218b5ab3c0a522d692ded4393a Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 14 Oct 2021 15:42:52 +0200 Subject: [PATCH] multi: forward SendCoins call over RPC --- go.mod | 3 +- go.sum | 6 +- lnrpc/walletrpc/walletkit_server.go | 2 +- lntest/mock/walletcontroller.go | 2 +- lnwallet/btcwallet/btcwallet.go | 7 +- lnwallet/interface.go | 4 +- lnwallet/rpcwallet/rpcwallet.go | 105 ++++++++++++++++++++++++++++ rpcserver.go | 2 +- 8 files changed, 117 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index 1502026b6..e99420cc2 100644 --- a/go.mod +++ b/go.mod @@ -9,10 +9,9 @@ require ( github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f github.com/btcsuite/btcutil v1.0.3-0.20210527170813-e2ba6805a890 github.com/btcsuite/btcutil/psbt v1.0.3-0.20210527170813-e2ba6805a890 - github.com/btcsuite/btcwallet v0.12.1-0.20210916213031-d0868cb9dd94 + github.com/btcsuite/btcwallet v0.12.1-0.20211008000044-541a8512ccfe github.com/btcsuite/btcwallet/wallet/txauthor v1.1.0 github.com/btcsuite/btcwallet/wallet/txrules v1.1.0 - github.com/btcsuite/btcwallet/wallet/txsizes v1.1.0 // indirect github.com/btcsuite/btcwallet/walletdb v1.3.6-0.20210803004036-eebed51155ec github.com/btcsuite/btcwallet/wtxmgr v1.3.1-0.20210822222949-9b5a201c344c github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f diff --git a/go.sum b/go.sum index 6ad1c41df..c8947059f 100644 --- a/go.sum +++ b/go.sum @@ -91,17 +91,15 @@ github.com/btcsuite/btcutil v1.0.3-0.20210527170813-e2ba6805a890/go.mod h1:0DVlH github.com/btcsuite/btcutil/psbt v1.0.3-0.20201208143702-a53e38424cce/go.mod h1:LVveMu4VaNSkIRTZu2+ut0HDBRuYjqGocxDMNS1KuGQ= github.com/btcsuite/btcutil/psbt v1.0.3-0.20210527170813-e2ba6805a890 h1:0xUNvvwJ7RjzBs4nCF+YrK28S5P/b4uHkpPxY1ovGY4= github.com/btcsuite/btcutil/psbt v1.0.3-0.20210527170813-e2ba6805a890/go.mod h1:LVveMu4VaNSkIRTZu2+ut0HDBRuYjqGocxDMNS1KuGQ= -github.com/btcsuite/btcwallet v0.12.1-0.20210916213031-d0868cb9dd94 h1:Bx+xu606h2sZNn5VaZMWvI0GtlGE+y+dHI4hbL5Ld6k= -github.com/btcsuite/btcwallet v0.12.1-0.20210916213031-d0868cb9dd94/go.mod h1:gHFk6GQ4IP/a8z6mfwK85GagUPxvAxCmgFy/whrBXhI= +github.com/btcsuite/btcwallet v0.12.1-0.20211008000044-541a8512ccfe h1:G7l/t3Y+B7jhRzcKs9z00KdocqdEqaXstwa0KSxH1bQ= +github.com/btcsuite/btcwallet v0.12.1-0.20211008000044-541a8512ccfe/go.mod h1:iLN1lG1MW0eREm+SikmPO8AZPz5NglBTEK/ErqkjGpo= github.com/btcsuite/btcwallet/wallet/txauthor v1.0.0/go.mod h1:VufDts7bd/zs3GV13f/lXc/0lXrPnvxD/NvmpG/FEKU= -github.com/btcsuite/btcwallet/wallet/txauthor v1.0.1-0.20210329233242-e0607006dce6/go.mod h1:VufDts7bd/zs3GV13f/lXc/0lXrPnvxD/NvmpG/FEKU= github.com/btcsuite/btcwallet/wallet/txauthor v1.1.0 h1:8pO0pvPX1rFRfRiol4oV6kX7dY5y4chPwhfVwUfvwtk= github.com/btcsuite/btcwallet/wallet/txauthor v1.1.0/go.mod h1:ktYuJyumYtwG+QQ832Q+kqvxWJRAei3Nqs5qhSn4nww= github.com/btcsuite/btcwallet/wallet/txrules v1.0.0/go.mod h1:UwQE78yCerZ313EXZwEiu3jNAtfXj2n2+c8RWiE/WNA= github.com/btcsuite/btcwallet/wallet/txrules v1.1.0 h1:Vg8G8zhNVjaCdwJg2QOmLoWn4RTP7K0J9xlwY8CJnLY= github.com/btcsuite/btcwallet/wallet/txrules v1.1.0/go.mod h1:Zn9UTqpiTH+HOd5BLzSBzULzlOPmcoeyQIA0cp0WbQQ= github.com/btcsuite/btcwallet/wallet/txsizes v1.0.0/go.mod h1:pauEU8UuMFiThe5PB3EO+gO5kx87Me5NvdQDsTuq6cs= -github.com/btcsuite/btcwallet/wallet/txsizes v1.0.1-0.20210519225359-6ab9b615576f/go.mod h1:pauEU8UuMFiThe5PB3EO+gO5kx87Me5NvdQDsTuq6cs= github.com/btcsuite/btcwallet/wallet/txsizes v1.1.0 h1:wZnOolEAeNOHzHTnznw/wQv+j35ftCIokNrnOTOU5o8= github.com/btcsuite/btcwallet/wallet/txsizes v1.1.0/go.mod h1:pauEU8UuMFiThe5PB3EO+gO5kx87Me5NvdQDsTuq6cs= github.com/btcsuite/btcwallet/walletdb v1.3.4/go.mod h1:oJDxAEUHVtnmIIBaa22wSBPTVcs6hUp5NKWmI8xDwwU= diff --git a/lnrpc/walletrpc/walletkit_server.go b/lnrpc/walletrpc/walletkit_server.go index 90025fdc0..364b373d0 100644 --- a/lnrpc/walletrpc/walletkit_server.go +++ b/lnrpc/walletrpc/walletkit_server.go @@ -1118,7 +1118,7 @@ func (w *WalletKit) FundPsbt(_ context.Context, // lock any coins but might still change the wallet DB by // generating a new change address. changeIndex, err = w.cfg.Wallet.FundPsbt( - packet, feeSatPerKW, account, + packet, minConfs, feeSatPerKW, account, ) if err != nil { return fmt.Errorf("wallet couldn't fund PSBT: %v", err) diff --git a/lntest/mock/walletcontroller.go b/lntest/mock/walletcontroller.go index 50eddd1ce..959fb5382 100644 --- a/lntest/mock/walletcontroller.go +++ b/lntest/mock/walletcontroller.go @@ -169,7 +169,7 @@ func (w *WalletController) ListLeasedOutputs() ([]*wtxmgr.LockedOutput, error) { } // FundPsbt currently does nothing. -func (w *WalletController) FundPsbt(_ *psbt.Packet, +func (w *WalletController) FundPsbt(_ *psbt.Packet, _ int32, _ chainfee.SatPerKWeight, _ string) (int32, error) { return 0, nil diff --git a/lnwallet/btcwallet/btcwallet.go b/lnwallet/btcwallet/btcwallet.go index 0dd38da04..3a2fb5f0a 100644 --- a/lnwallet/btcwallet/btcwallet.go +++ b/lnwallet/btcwallet/btcwallet.go @@ -642,7 +642,8 @@ func (b *BtcWallet) ImportPublicKey(pubKey *btcec.PublicKey, // // This is a part of the WalletController interface. func (b *BtcWallet) SendOutputs(outputs []*wire.TxOut, - feeRate chainfee.SatPerKWeight, minConfs int32, label string) (*wire.MsgTx, error) { + feeRate chainfee.SatPerKWeight, minConfs int32, + label string) (*wire.MsgTx, error) { // Convert our fee rate from sat/kw to sat/kb since it's required by // SendOutputs. @@ -1080,7 +1081,7 @@ func (b *BtcWallet) ListTransactionDetails(startHeight, endHeight int32, // responsibility to lock the inputs before handing them out. // // This is a part of the WalletController interface. -func (b *BtcWallet) FundPsbt(packet *psbt.Packet, +func (b *BtcWallet) FundPsbt(packet *psbt.Packet, minConfs int32, feeRate chainfee.SatPerKWeight, accountName string) (int32, error) { // The fee rate is passed in using units of sat/kw, so we'll convert @@ -1115,7 +1116,7 @@ func (b *BtcWallet) FundPsbt(packet *psbt.Packet, // Let the wallet handle coin selection and/or fee estimation based on // the partial TX information in the packet. return b.wallet.FundPsbt( - packet, keyScope, accountNum, feeSatPerKB, + packet, keyScope, minConfs, accountNum, feeSatPerKB, b.cfg.CoinSelectionStrategy, ) } diff --git a/lnwallet/interface.go b/lnwallet/interface.go index b15fa7a03..42c2f633a 100644 --- a/lnwallet/interface.go +++ b/lnwallet/interface.go @@ -350,8 +350,8 @@ type WalletController interface { // fee rate, an error is returned. No lock lease is acquired for any of // the selected/validated inputs. It is in the caller's responsibility // to lock the inputs before handing them out. - FundPsbt(packet *psbt.Packet, feeRate chainfee.SatPerKWeight, - account string) (int32, error) + FundPsbt(packet *psbt.Packet, minConfs int32, + feeRate chainfee.SatPerKWeight, account string) (int32, error) // FinalizePsbt expects a partial transaction with all inputs and // outputs fully declared and tries to sign all inputs that belong to diff --git a/lnwallet/rpcwallet/rpcwallet.go b/lnwallet/rpcwallet/rpcwallet.go index 61c4d9526..9b5b12e61 100644 --- a/lnwallet/rpcwallet/rpcwallet.go +++ b/lnwallet/rpcwallet/rpcwallet.go @@ -11,16 +11,20 @@ import ( "time" "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil/hdkeychain" "github.com/btcsuite/btcwallet/waddrmgr" + btcwallet "github.com/btcsuite/btcwallet/wallet" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lncfg" "github.com/lightningnetwork/lnd/lnrpc/signrpc" "github.com/lightningnetwork/lnd/lnrpc/walletrpc" "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/lnwallet/chainfee" + "github.com/lightningnetwork/lnd/lnwallet/chanfunding" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/macaroons" "google.golang.org/grpc" @@ -139,6 +143,26 @@ func (r *RPCKeyRing) NewAddress(addrType lnwallet.AddressType, change bool, return localAddr, nil } +// LastUnusedAddress returns the last *unused* address known by the wallet. An +// address is unused if it hasn't received any payments. This can be useful in +// UIs in order to continually show the "freshest" address without having to +// worry about "address inflation" caused by continual refreshing. Similar to +// NewAddress it can derive a specified address type, and also optionally a +// change address. The account parameter must be non-empty as it determines +// which account the address should be generated from. +func (r *RPCKeyRing) LastUnusedAddress(lnwallet.AddressType, + string) (btcutil.Address, error) { + + // Because the underlying wallet will create a new address if the last + // derived address has been used in the meantime, we would need to proxy + // that call as well. But since that's deep within the btcwallet code, + // we cannot easily proxy it without more refactoring. Since this is an + // address type that is probably not widely used we can probably get + // away with not supporting it. + return nil, fmt.Errorf("unused address types are not supported when " + + "remote signing is enabled") +} + // ImportAccount imports an account backed by an account extended public key. // The master key fingerprint denotes the fingerprint of the root key // corresponding to the account public key (also known as the key with @@ -265,6 +289,67 @@ func (r *RPCKeyRing) ImportPublicKey(pubKey *btcec.PublicKey, return nil } +// SendOutputs funds, signs, and broadcasts a Bitcoin transaction paying out to +// the specified outputs. In the case the wallet has insufficient funds, or the +// outputs are non-standard, a non-nil error will be returned. +// +// NOTE: This method requires the global coin selection lock to be held. +// +// This is a part of the WalletController interface. +func (r *RPCKeyRing) SendOutputs(outputs []*wire.TxOut, + feeRate chainfee.SatPerKWeight, minConfs int32, + label string) (*wire.MsgTx, error) { + + tx, err := r.WalletController.SendOutputs( + outputs, feeRate, minConfs, label, + ) + if err != nil && err != btcwallet.ErrTxUnsigned { + return nil, err + } + if err == nil { + // This shouldn't happen since our wallet controller is watch- + // only and can't sign the TX. + return tx, nil + } + + // We know at this point that we only have inputs from our own wallet. + // So we can just compute the input script using the remote signer. + signDesc := input.SignDescriptor{ + HashType: txscript.SigHashAll, + SigHashes: txscript.NewTxSigHashes(tx), + } + for i, txIn := range tx.TxIn { + // We can only sign this input if it's ours, so we'll ask the + // watch-only wallet if it can map this outpoint into a coin we + // own. If not, then we can't continue because our wallet state + // is out of sync. + info, err := r.coinFromOutPoint(txIn.PreviousOutPoint) + if err != nil { + return nil, fmt.Errorf("error looking up utxo: %v", err) + } + + // Now that we know the input is ours, we'll populate the + // signDesc with the per input unique information. + signDesc.Output = &wire.TxOut{ + Value: info.Value, + PkScript: info.PkScript, + } + signDesc.InputIndex = i + + // Finally, we'll sign the input as is, and populate the input + // with the witness and sigScript (if needed). + inputScript, err := r.ComputeInputScript(tx, &signDesc) + if err != nil { + return nil, err + } + + txIn.SignatureScript = inputScript.SigScript + txIn.Witness = inputScript.Witness + } + + return tx, r.WalletController.PublishTransaction(tx, label) +} + // DeriveNextKey attempts to derive the *next* key within the key family // (account in BIP43) specified. This method should return the next external // child within this branch. @@ -534,6 +619,26 @@ func (r *RPCKeyRing) ComputeInputScript(tx *wire.MsgTx, }, nil } +// coinFromOutPoint attempts to locate details pertaining to a coin based on +// its outpoint. If the coin isn't under the control of the backing watch-only +// wallet, then an error is returned. +func (r *RPCKeyRing) coinFromOutPoint(op wire.OutPoint) (*chanfunding.Coin, + error) { + + inputInfo, err := r.WalletController.FetchInputInfo(&op) + if err != nil { + return nil, err + } + + return &chanfunding.Coin{ + TxOut: wire.TxOut{ + Value: int64(inputInfo.Value), + PkScript: inputInfo.PkScript, + }, + OutPoint: inputInfo.OutPoint, + }, nil +} + // toRPCSignReq converts the given raw transaction and sign descriptors into // their corresponding RPC counterparts. func toRPCSignReq(tx *wire.MsgTx, diff --git a/rpcserver.go b/rpcserver.go index d56067a9e..85346b544 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -1037,7 +1037,7 @@ func (r *rpcServer) sendCoinsOnChain(paymentMap map[string]int64, return nil, err } - // If that checks out, we're failry confident that creating sending to + // If that checks out, we're fairly confident that creating sending to // these outputs will keep the wallet balance above the reserve. tx, err := r.server.cc.Wallet.SendOutputs( outputs, feeRate, minConfs, label,