From c70858dc46658d5c040be33e16b76ebee6a3ebc6 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Wed, 29 May 2019 13:59:00 -0700 Subject: [PATCH 1/6] sweep: prevent default fee preference fallback We want to make sure clients are aware of their own fee preferences, rather than relying on defaults. --- sweep/sweeper.go | 10 ++++++++++ sweep/sweeper_test.go | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/sweep/sweeper.go b/sweep/sweeper.go index 6fe398559..2f9f9b190 100644 --- a/sweep/sweeper.go +++ b/sweep/sweeper.go @@ -46,6 +46,10 @@ var ( // for the configured max number of attempts. ErrTooManyAttempts = errors.New("sweep failed after max attempts") + // ErrNoFeePreference is returned when we attempt to satisfy a sweep + // request from a client whom did not specify a fee preference. + ErrNoFeePreference = errors.New("no fee preference specified") + // ErrSweeperShuttingDown is an error returned when a client attempts to // make a request to the UtxoSweeper, but it is unable to handle it as // it is/has already been stoppepd. @@ -394,6 +398,12 @@ func (s *UtxoSweeper) SweepInput(input input.Input, func (s *UtxoSweeper) feeRateForPreference( feePreference FeePreference) (lnwallet.SatPerKWeight, error) { + // Ensure a type of fee preference is specified to prevent using a + // default below. + if feePreference.FeeRate == 0 && feePreference.ConfTarget == 0 { + return 0, ErrNoFeePreference + } + feeRate, err := DetermineFeePerKw(s.cfg.FeeEstimator, feePreference) if err != nil { return 0, err diff --git a/sweep/sweeper_test.go b/sweep/sweeper_test.go index d494b996b..6d0999014 100644 --- a/sweep/sweeper_test.go +++ b/sweep/sweeper_test.go @@ -372,6 +372,12 @@ func assertTxFeeRate(t *testing.T, tx *wire.MsgTx, func TestSuccess(t *testing.T) { ctx := createSweeperTestContext(t) + // Sweeping an input without a fee preference should result in an error. + _, err := ctx.sweeper.SweepInput( spendableInputs[0], FeePreference{}) + if err != ErrNoFeePreference { + t.Fatalf("expected ErrNoFeePreference, got %v", err) + } + resultChan, err := ctx.sweeper.SweepInput( spendableInputs[0], defaultFeePref, ) From f206444e96b44980dcb943fdba296eaf4b9aec1a Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Wed, 29 May 2019 13:59:37 -0700 Subject: [PATCH 2/6] sweep: assert transaction fee rate in TestDifferentFeePreferences --- sweep/sweeper_test.go | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/sweep/sweeper_test.go b/sweep/sweeper_test.go index 6d0999014..9926cd478 100644 --- a/sweep/sweeper_test.go +++ b/sweep/sweeper_test.go @@ -1013,14 +1013,13 @@ func TestDifferentFeePreferences(t *testing.T) { // with the higher fee preference, and the last with the lower. We do // this to ensure the sweeper can broadcast distinct transactions for // each sweep with a different fee preference. - lowFeePref := FeePreference{ - ConfTarget: 12, - } - ctx.estimator.blocksToFee[lowFeePref.ConfTarget] = 5000 - highFeePref := FeePreference{ - ConfTarget: 6, - } - ctx.estimator.blocksToFee[highFeePref.ConfTarget] = 10000 + lowFeePref := FeePreference{ConfTarget: 12} + lowFeeRate := lnwallet.SatPerKWeight(5000) + ctx.estimator.blocksToFee[lowFeePref.ConfTarget] = lowFeeRate + + highFeePref := FeePreference{ConfTarget: 6} + highFeeRate := lnwallet.SatPerKWeight(10000) + ctx.estimator.blocksToFee[highFeePref.ConfTarget] = highFeeRate input1 := spendableInputs[0] resultChan1, err := ctx.sweeper.SweepInput(input1, highFeePref) @@ -1045,11 +1044,11 @@ func TestDifferentFeePreferences(t *testing.T) { // The first transaction broadcast should be the one spending the higher // fee rate inputs. sweepTx1 := ctx.receiveTx() - assertTxSweepsInputs(t, &sweepTx1, input1, input2) + assertTxFeeRate(t, &sweepTx1, highFeeRate, input1, input2) // The second should be the one spending the lower fee rate inputs. sweepTx2 := ctx.receiveTx() - assertTxSweepsInputs(t, &sweepTx2, input3) + assertTxFeeRate(t, &sweepTx2, lowFeeRate, input3) // With the transactions broadcast, we'll mine a block to so that the // result is delivered to each respective client. From e69d93949c7426f136dcd1309ff7e3938157e0b0 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Wed, 29 May 2019 14:00:14 -0700 Subject: [PATCH 3/6] sweep: allow fee bumps of inputs/transactions within UtxoSweeper In this commit, we introduce the ability to bump the fee of an input within the UtxoSweeper. Once its fee rate is bumped, a replacement transaction (RBF) will be broadcast with the newer fee rate (assuming the newer fee rate is high enough to be valid), replacing any conflicting lower fee rate transactions. Note that this currently doesn't validate the fee preference of the bump. This responsibility is delegated to the caller, so care must be taken to ensure the new fee preference is sufficient. --- sweep/sweeper.go | 122 +++++++++++++++++++++++++++++++++++++++++- sweep/sweeper_test.go | 63 +++++++++++++++++++++- 2 files changed, 182 insertions(+), 3 deletions(-) diff --git a/sweep/sweeper.go b/sweep/sweeper.go index 2f9f9b190..03fcf6ca7 100644 --- a/sweep/sweeper.go +++ b/sweep/sweeper.go @@ -22,7 +22,7 @@ const ( // DefaultMaxFeeRate is the default maximum fee rate allowed within the // UtxoSweeper. The current value is equivalent to a fee rate of 10,000 // sat/vbyte. - DefaultMaxFeeRate lnwallet.SatPerKWeight = 250 * 1e4 + DefaultMaxFeeRate = lnwallet.FeePerKwFloor * 1e4 // DefaultFeeRateBucketSize is the default size of fee rate buckets // we'll use when clustering inputs into buckets with similar fee rates @@ -137,6 +137,21 @@ type PendingInput struct { NextBroadcastHeight uint32 } +// bumpFeeReq is an internal message we'll use to represent an external caller's +// intent to bump the fee rate of a given input. +type bumpFeeReq struct { + input wire.OutPoint + feePreference FeePreference + responseChan chan *bumpFeeResp +} + +// bumpFeeResp is an internal message we'll use to hand off the response of a +// bumpFeeReq from the UtxoSweeper's main event loop back to the caller. +type bumpFeeResp struct { + resultChan chan Result + err error +} + // UtxoSweeper is responsible for sweeping outputs back into the wallet type UtxoSweeper struct { started uint32 // To be used atomically. @@ -152,6 +167,10 @@ type UtxoSweeper struct { // UtxoSweeper is attempting to sweep. pendingSweepsReqs chan *pendingSweepsReq + // bumpFeeReqs is a channel that will be sent requests by external + // callers who wish to bump the fee rate of a given input. + bumpFeeReqs chan *bumpFeeReq + // pendingInputs is the total set of inputs the UtxoSweeper has been // requested to sweep. pendingInputs pendingInputs @@ -261,6 +280,7 @@ func New(cfg *UtxoSweeperConfig) *UtxoSweeper { cfg: cfg, newInputs: make(chan *sweepInputMessage), spendChan: make(chan *chainntnfs.SpendDetail), + bumpFeeReqs: make(chan *bumpFeeReq), pendingSweepsReqs: make(chan *pendingSweepsReq), quit: make(chan struct{}), pendingInputs: make(pendingInputs), @@ -355,7 +375,10 @@ func (s *UtxoSweeper) Stop() error { // SweepInput sweeps inputs back into the wallet. The inputs will be batched and // swept after the batch time window ends. A custom fee preference can be -// provided, otherwise the UtxoSweeper's default will be used. +// provided to determine what fee rate should be used for the input. Note that +// the input may not always be swept with this exact value, as its possible for +// it to be batched under the same transaction with other similar fee rate +// inputs. // // NOTE: Extreme care needs to be taken that input isn't changed externally. // Because it is an interface and we don't know what is exactly behind it, we @@ -542,6 +565,15 @@ func (s *UtxoSweeper) collector(blockEpochs <-chan *chainntnfs.BlockEpoch, case req := <-s.pendingSweepsReqs: req.respChan <- s.handlePendingSweepsReq(req) + // A new external request has been received to bump the fee rate + // of a given input. + case req := <-s.bumpFeeReqs: + resultChan, err := s.handleBumpFeeReq(req, bestHeight) + req.responseChan <- &bumpFeeResp{ + resultChan: resultChan, + err: err, + } + // The timer expires and we are going to (re)sweep. case <-s.timer: log.Debugf("Sweep timer expired") @@ -989,6 +1021,92 @@ func (s *UtxoSweeper) handlePendingSweepsReq( return pendingInputs } +// BumpFee allows bumping the fee of an input being swept by the UtxoSweeper +// according to the provided fee preference. The new fee preference will be used +// for a new sweep transaction of the input that will act as a replacement +// transaction (RBF) of the original sweeping transaction, if any. +// +// NOTE: This currently doesn't do any fee rate validation to ensure that a bump +// is actually successful. The responsibility of doing so should be handled by +// the caller. +func (s *UtxoSweeper) BumpFee(input wire.OutPoint, + feePreference FeePreference) (chan Result, error) { + + // Ensure the client provided a sane fee preference. + if _, err := s.feeRateForPreference(feePreference); err != nil { + return nil, err + } + + responseChan := make(chan *bumpFeeResp, 1) + select { + case s.bumpFeeReqs <- &bumpFeeReq{ + input: input, + feePreference: feePreference, + responseChan: responseChan, + }: + case <-s.quit: + return nil, ErrSweeperShuttingDown + } + + select { + case response := <-responseChan: + return response.resultChan, response.err + case <-s.quit: + return nil, ErrSweeperShuttingDown + } +} + +// handleBumpFeeReq handles a bump fee request by simply updating the inputs fee +// preference. Currently, no validation is done on the new fee preference to +// ensure it will properly create a replacement transaction. +// +// TODO(wilmer): +// * Validate fee preference to ensure we'll create a valid replacement +// transaction to allow the new fee rate to propagate throughout the +// network. +// * Ensure we don't combine this input with any other unconfirmed inputs that +// did not exist in the original sweep transaction, resulting in an invalid +// replacement transaction. +func (s *UtxoSweeper) handleBumpFeeReq(req *bumpFeeReq, + bestHeight int32) (chan Result, error) { + + // If the UtxoSweeper is already trying to sweep this input, then we can + // simply just increase its fee rate. This will allow the input to be + // batched with others which also have a similar fee rate, creating a + // higher fee rate transaction that replaces the original input's + // sweeping transaction. + pendingInput, ok := s.pendingInputs[req.input] + if !ok { + return nil, lnwallet.ErrNotMine + } + + log.Debugf("Updating fee preference for %v from %v to %v", req.input, + pendingInput.feePreference, req.feePreference) + + pendingInput.feePreference = req.feePreference + + // We'll reset the input's publish height to the current so that a new + // transaction can be created that replaces the transaction currently + // spending the input. We only do this for inputs that have been + // broadcast at least once to ensure we don't spend an input before its + // maturity height. + // + // NOTE: The UtxoSweeper is not yet offered time-locked inputs, so the + // check for broadcast attempts is redundant at the moment. + if pendingInput.publishAttempts > 0 { + pendingInput.minPublishHeight = bestHeight + } + + if err := s.scheduleSweep(bestHeight); err != nil { + log.Errorf("Unable to schedule sweep: %v", err) + } + + resultChan := make(chan Result, 1) + pendingInput.listeners = append(pendingInput.listeners, resultChan) + + return resultChan, nil +} + // CreateSweepTx accepts a list of inputs and signs and generates a txn that // spends from them. This method also makes an accurate fee estimate before // generating the required witnesses. diff --git a/sweep/sweeper_test.go b/sweep/sweeper_test.go index 9926cd478..8f72a7abf 100644 --- a/sweep/sweeper_test.go +++ b/sweep/sweeper_test.go @@ -373,7 +373,7 @@ func TestSuccess(t *testing.T) { ctx := createSweeperTestContext(t) // Sweeping an input without a fee preference should result in an error. - _, err := ctx.sweeper.SweepInput( spendableInputs[0], FeePreference{}) + _, err := ctx.sweeper.SweepInput(spendableInputs[0], FeePreference{}) if err != ErrNoFeePreference { t.Fatalf("expected ErrNoFeePreference, got %v", err) } @@ -1128,3 +1128,64 @@ func TestPendingInputs(t *testing.T) { ctx.finish(1) } + +// TestBumpFeeRBF ensures that the UtxoSweeper can properly handle a fee bump +// request for an input it is currently attempting to sweep. When sweeping the +// input with the higher fee rate, a replacement transaction is created. +func TestBumpFeeRBF(t *testing.T) { + ctx := createSweeperTestContext(t) + + lowFeePref := FeePreference{ConfTarget: 144} + lowFeeRate := lnwallet.FeePerKwFloor + ctx.estimator.blocksToFee[lowFeePref.ConfTarget] = lowFeeRate + + // We'll first try to bump the fee of an output currently unknown to the + // UtxoSweeper. Doing so should result in a lnwallet.ErrNotMine error. + bumpResult, err := ctx.sweeper.BumpFee(wire.OutPoint{}, lowFeePref) + if err != lnwallet.ErrNotMine { + t.Fatalf("expected error lnwallet.ErrNotMine, got \"%v\"", err) + } + + // We'll then attempt to sweep an input, which we'll use to bump its fee + // later on. + input := createTestInput( + btcutil.SatoshiPerBitcoin, input.CommitmentTimeLock, + ) + sweepResult, err := ctx.sweeper.SweepInput(&input, lowFeePref) + if err != nil { + t.Fatal(err) + } + + // Ensure that a transaction is broadcast with the lower fee preference. + ctx.tick() + lowFeeTx := ctx.receiveTx() + assertTxFeeRate(t, &lowFeeTx, lowFeeRate, &input) + + // We'll then attempt to bump its fee rate. + highFeePref := FeePreference{ConfTarget: 6} + highFeeRate := DefaultMaxFeeRate + ctx.estimator.blocksToFee[highFeePref.ConfTarget] = highFeeRate + + // We should expect to see an error if a fee preference isn't provided. + _, err = ctx.sweeper.BumpFee(*input.OutPoint(), FeePreference{}) + if err != ErrNoFeePreference { + t.Fatalf("expected ErrNoFeePreference, got %v", err) + } + + bumpResult, err = ctx.sweeper.BumpFee(*input.OutPoint(), highFeePref) + if err != nil { + t.Fatalf("unable to bump input's fee: %v", err) + } + + // A higher fee rate transaction should be immediately broadcast. + ctx.tick() + highFeeTx := ctx.receiveTx() + assertTxFeeRate(t, &highFeeTx, highFeeRate, &input) + + // We'll finish our test by mining the sweep transaction. + ctx.backend.mine() + ctx.expectResult(sweepResult, nil) + ctx.expectResult(bumpResult, nil) + + ctx.finish(1) +} From a4675063b7e897ddfb7d22f3ef8fddf915617a49 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Wed, 29 May 2019 14:00:29 -0700 Subject: [PATCH 4/6] lnrpc/walletrpc: expose bumping fee of inputs/transactions over RPC This RPC exposes the recently added BumpFee functionality to the UtxoSweeper in order to allow users of the RPC to manually bump fees of low fee inputs/transactions. --- lnrpc/walletrpc/walletkit.pb.go | 299 ++++++++++++++++++++++------ lnrpc/walletrpc/walletkit.proto | 46 +++++ lnrpc/walletrpc/walletkit_server.go | 110 ++++++++++ 3 files changed, 396 insertions(+), 59 deletions(-) diff --git a/lnrpc/walletrpc/walletkit.pb.go b/lnrpc/walletrpc/walletkit.pb.go index ffdb00bc5..ac8266a47 100644 --- a/lnrpc/walletrpc/walletkit.pb.go +++ b/lnrpc/walletrpc/walletkit.pb.go @@ -676,6 +676,97 @@ func (m *PendingSweepsResponse) GetPendingSweeps() []*PendingSweep { return nil } +type BumpFeeRequest struct { + // The input we're attempting to bump the fee of. + Outpoint *lnrpc.OutPoint `protobuf:"bytes,1,opt,name=outpoint,proto3" json:"outpoint,omitempty"` + // The target number of blocks that the input should be spent within. + TargetConf uint32 `protobuf:"varint,2,opt,name=target_conf,proto3" json:"target_conf,omitempty"` + // + //The fee rate, expressed in sat/byte, that should be used to spend the input + //with. + SatPerByte uint32 `protobuf:"varint,3,opt,name=sat_per_byte,proto3" json:"sat_per_byte,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *BumpFeeRequest) Reset() { *m = BumpFeeRequest{} } +func (m *BumpFeeRequest) String() string { return proto.CompactTextString(m) } +func (*BumpFeeRequest) ProtoMessage() {} +func (*BumpFeeRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_6cc6942ac78249e5, []int{12} +} + +func (m *BumpFeeRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_BumpFeeRequest.Unmarshal(m, b) +} +func (m *BumpFeeRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_BumpFeeRequest.Marshal(b, m, deterministic) +} +func (m *BumpFeeRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_BumpFeeRequest.Merge(m, src) +} +func (m *BumpFeeRequest) XXX_Size() int { + return xxx_messageInfo_BumpFeeRequest.Size(m) +} +func (m *BumpFeeRequest) XXX_DiscardUnknown() { + xxx_messageInfo_BumpFeeRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_BumpFeeRequest proto.InternalMessageInfo + +func (m *BumpFeeRequest) GetOutpoint() *lnrpc.OutPoint { + if m != nil { + return m.Outpoint + } + return nil +} + +func (m *BumpFeeRequest) GetTargetConf() uint32 { + if m != nil { + return m.TargetConf + } + return 0 +} + +func (m *BumpFeeRequest) GetSatPerByte() uint32 { + if m != nil { + return m.SatPerByte + } + return 0 +} + +type BumpFeeResponse struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *BumpFeeResponse) Reset() { *m = BumpFeeResponse{} } +func (m *BumpFeeResponse) String() string { return proto.CompactTextString(m) } +func (*BumpFeeResponse) ProtoMessage() {} +func (*BumpFeeResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_6cc6942ac78249e5, []int{13} +} + +func (m *BumpFeeResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_BumpFeeResponse.Unmarshal(m, b) +} +func (m *BumpFeeResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_BumpFeeResponse.Marshal(b, m, deterministic) +} +func (m *BumpFeeResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_BumpFeeResponse.Merge(m, src) +} +func (m *BumpFeeResponse) XXX_Size() int { + return xxx_messageInfo_BumpFeeResponse.Size(m) +} +func (m *BumpFeeResponse) XXX_DiscardUnknown() { + xxx_messageInfo_BumpFeeResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_BumpFeeResponse proto.InternalMessageInfo + func init() { proto.RegisterEnum("walletrpc.WitnessType", WitnessType_name, WitnessType_value) proto.RegisterType((*KeyReq)(nil), "walletrpc.KeyReq") @@ -690,70 +781,75 @@ func init() { proto.RegisterType((*PendingSweep)(nil), "walletrpc.PendingSweep") proto.RegisterType((*PendingSweepsRequest)(nil), "walletrpc.PendingSweepsRequest") proto.RegisterType((*PendingSweepsResponse)(nil), "walletrpc.PendingSweepsResponse") + proto.RegisterType((*BumpFeeRequest)(nil), "walletrpc.BumpFeeRequest") + proto.RegisterType((*BumpFeeResponse)(nil), "walletrpc.BumpFeeResponse") } func init() { proto.RegisterFile("walletrpc/walletkit.proto", fileDescriptor_6cc6942ac78249e5) } var fileDescriptor_6cc6942ac78249e5 = []byte{ - // 918 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x55, 0x5d, 0x6f, 0xe2, 0x46, - 0x14, 0x2d, 0x21, 0x61, 0xc3, 0x05, 0x12, 0xef, 0x10, 0x12, 0x97, 0xcd, 0x6e, 0xa8, 0xfb, 0x21, - 0xd4, 0x56, 0xa0, 0x66, 0xdb, 0xaa, 0x6a, 0x1f, 0xaa, 0x14, 0x1c, 0x11, 0xf1, 0x61, 0x6a, 0x3b, - 0x9b, 0x6e, 0x55, 0x69, 0x64, 0x60, 0x16, 0x2c, 0xc0, 0x76, 0xc6, 0x43, 0xc1, 0xaf, 0xfd, 0x27, - 0xfd, 0x97, 0x7d, 0xac, 0x3c, 0xb6, 0xc9, 0x98, 0x24, 0xfb, 0x14, 0xe7, 0x9c, 0x73, 0xcf, 0xdc, - 0xb9, 0x33, 0x73, 0x80, 0x4f, 0xd7, 0xd6, 0x62, 0x41, 0x18, 0xf5, 0xc6, 0xcd, 0xe8, 0x6b, 0x6e, - 0xb3, 0x86, 0x47, 0x5d, 0xe6, 0xa2, 0xfc, 0x96, 0xaa, 0xe6, 0xa9, 0x37, 0x8e, 0xd0, 0xea, 0x89, - 0x6f, 0x4f, 0x9d, 0x50, 0x1e, 0xfe, 0x25, 0x34, 0x42, 0x95, 0xdf, 0x21, 0xd7, 0x25, 0x81, 0x4e, - 0xee, 0x51, 0x1d, 0xa4, 0x39, 0x09, 0xf0, 0x07, 0xdb, 0x99, 0x12, 0x8a, 0x3d, 0x6a, 0x3b, 0x4c, - 0xce, 0xd4, 0x32, 0xf5, 0x03, 0xfd, 0x68, 0x4e, 0x82, 0x6b, 0x0e, 0x0f, 0x43, 0x14, 0xbd, 0x06, - 0xe0, 0x4a, 0x6b, 0x69, 0x2f, 0x02, 0x79, 0x8f, 0x6b, 0xf2, 0xa1, 0x86, 0x03, 0x4a, 0x09, 0x0a, - 0x57, 0x93, 0x09, 0xd5, 0xc9, 0xfd, 0x8a, 0xf8, 0x4c, 0x51, 0xa0, 0x18, 0xfd, 0xeb, 0x7b, 0xae, - 0xe3, 0x13, 0x84, 0x60, 0xdf, 0x9a, 0x4c, 0x28, 0xf7, 0xce, 0xeb, 0xfc, 0x5b, 0xf9, 0x02, 0x0a, - 0x26, 0xb5, 0x1c, 0xdf, 0x1a, 0x33, 0xdb, 0x75, 0x50, 0x05, 0x72, 0x6c, 0x83, 0x67, 0x64, 0xc3, - 0x45, 0x45, 0xfd, 0x80, 0x6d, 0x3a, 0x64, 0xa3, 0xfc, 0x08, 0xc7, 0xc3, 0xd5, 0x68, 0x61, 0xfb, - 0xb3, 0xad, 0xd9, 0xe7, 0x50, 0xf2, 0x22, 0x08, 0x13, 0x4a, 0xdd, 0xc4, 0xb5, 0x18, 0x83, 0x6a, - 0x88, 0x29, 0x7f, 0x01, 0x32, 0x88, 0x33, 0xd1, 0x56, 0xcc, 0x5b, 0x31, 0x3f, 0xee, 0x0b, 0x9d, - 0x03, 0xf8, 0x16, 0xc3, 0x1e, 0xa1, 0x78, 0xbe, 0xe6, 0x75, 0x59, 0xfd, 0xd0, 0xb7, 0xd8, 0x90, - 0xd0, 0xee, 0x1a, 0xd5, 0xe1, 0x85, 0x1b, 0xe9, 0xe5, 0xbd, 0x5a, 0xb6, 0x5e, 0xb8, 0x3c, 0x6a, - 0xc4, 0xf3, 0x6b, 0x98, 0x1b, 0x6d, 0xc5, 0xf4, 0x84, 0x56, 0xbe, 0x85, 0x72, 0xca, 0x3d, 0xee, - 0xac, 0x02, 0x39, 0x6a, 0xad, 0x31, 0xdb, 0xee, 0x81, 0x5a, 0x6b, 0x73, 0xa3, 0xfc, 0x00, 0x48, - 0xf5, 0x99, 0xbd, 0xb4, 0x18, 0xb9, 0x26, 0x24, 0xe9, 0xe5, 0x02, 0x0a, 0x63, 0xd7, 0xf9, 0x80, - 0x99, 0x45, 0xa7, 0x24, 0x19, 0x3b, 0x84, 0x90, 0xc9, 0x11, 0xe5, 0x2d, 0x94, 0x53, 0x65, 0xf1, - 0x22, 0x1f, 0xdd, 0x83, 0xf2, 0xef, 0x1e, 0x14, 0x87, 0xc4, 0x99, 0xd8, 0xce, 0xd4, 0x58, 0x13, - 0xe2, 0xa1, 0x6f, 0xe0, 0x30, 0xec, 0xda, 0x4d, 0x8e, 0xb6, 0x70, 0x79, 0xdc, 0x58, 0xf0, 0x3d, - 0x69, 0x2b, 0x36, 0x0c, 0x61, 0x7d, 0x2b, 0x40, 0x3f, 0x43, 0x71, 0x6d, 0x33, 0x87, 0xf8, 0x3e, - 0x66, 0x81, 0x47, 0xf8, 0x39, 0x1f, 0x5d, 0x9e, 0x36, 0xb6, 0x97, 0xab, 0x71, 0x17, 0xd1, 0x66, - 0xe0, 0x11, 0x3d, 0xa5, 0x45, 0x6f, 0x00, 0xac, 0xa5, 0xbb, 0x72, 0x18, 0xf6, 0x2d, 0x26, 0x67, - 0x6b, 0x99, 0x7a, 0x49, 0x17, 0x10, 0xa4, 0x40, 0x31, 0xe9, 0x7b, 0x14, 0x30, 0x22, 0xef, 0x73, - 0x45, 0x0a, 0x43, 0x0d, 0x40, 0x23, 0xea, 0x5a, 0x93, 0xb1, 0xe5, 0x33, 0x6c, 0x31, 0x46, 0x96, - 0x1e, 0xf3, 0xe5, 0x03, 0xae, 0x7c, 0x82, 0x41, 0xdf, 0x43, 0xc5, 0x21, 0x1b, 0x86, 0x1f, 0xa8, - 0x19, 0xb1, 0xa7, 0x33, 0x26, 0xe7, 0x78, 0xc9, 0xd3, 0xa4, 0x72, 0x0a, 0x27, 0xe2, 0x88, 0x92, - 0xdb, 0xa1, 0xfc, 0x01, 0x95, 0x1d, 0x3c, 0x1e, 0xf9, 0xaf, 0x70, 0xe4, 0x45, 0x04, 0xf6, 0x39, - 0x23, 0x67, 0xf8, 0xfd, 0x38, 0x13, 0x06, 0x23, 0x56, 0xea, 0x3b, 0xf2, 0xaf, 0xff, 0xc9, 0x42, - 0x41, 0x98, 0x1c, 0x2a, 0xc3, 0xf1, 0xed, 0xa0, 0x3b, 0xd0, 0xee, 0x06, 0xf8, 0xee, 0xc6, 0x1c, - 0xa8, 0x86, 0x21, 0x7d, 0x82, 0x64, 0x38, 0x69, 0x69, 0xfd, 0xfe, 0x8d, 0xd9, 0x57, 0x07, 0x26, - 0x36, 0x6f, 0xfa, 0x2a, 0xee, 0x69, 0xad, 0xae, 0x94, 0x41, 0x67, 0x50, 0x16, 0x98, 0x81, 0x86, - 0xdb, 0x6a, 0xef, 0xea, 0xbd, 0xb4, 0x87, 0x2a, 0xf0, 0x52, 0x20, 0x74, 0xf5, 0x9d, 0xd6, 0x55, - 0xa5, 0x6c, 0xa8, 0xef, 0x98, 0xbd, 0x16, 0xd6, 0xae, 0xaf, 0x55, 0x5d, 0x6d, 0x27, 0xc4, 0x7e, - 0xb8, 0x04, 0x27, 0xae, 0x5a, 0x2d, 0x75, 0x68, 0x3e, 0x30, 0x07, 0xe8, 0x4b, 0xf8, 0x2c, 0x55, - 0x12, 0x2e, 0xaf, 0xdd, 0x9a, 0xd8, 0x50, 0x5b, 0xda, 0xa0, 0x8d, 0x7b, 0xea, 0x3b, 0xb5, 0x27, - 0xe5, 0xd0, 0x57, 0xa0, 0xa4, 0x0d, 0x8c, 0xdb, 0x56, 0x4b, 0x35, 0x8c, 0xb4, 0xee, 0x05, 0xba, - 0x80, 0x57, 0x3b, 0x1d, 0xf4, 0x35, 0x53, 0x4d, 0x5c, 0xa5, 0x43, 0x54, 0x83, 0xf3, 0xdd, 0x4e, - 0xb8, 0x22, 0xf6, 0x93, 0xf2, 0xe8, 0x1c, 0x64, 0xae, 0x10, 0x9d, 0x93, 0x7e, 0x01, 0x9d, 0x80, - 0x14, 0x4f, 0x0e, 0x77, 0xd5, 0xf7, 0xb8, 0x73, 0x65, 0x74, 0xa4, 0x02, 0x7a, 0x05, 0x67, 0x03, - 0xd5, 0x08, 0xed, 0x1e, 0x91, 0xc5, 0xcb, 0xff, 0xb2, 0x90, 0xbf, 0xe3, 0xe7, 0xd5, 0xb5, 0xc3, - 0xab, 0x5e, 0x6a, 0x13, 0x6a, 0xff, 0x4d, 0x06, 0x64, 0xc3, 0xba, 0x24, 0x40, 0x2f, 0x85, 0xc3, - 0x8c, 0xe2, 0xb1, 0x7a, 0xba, 0x7d, 0xff, 0x5d, 0x12, 0xb4, 0x89, 0x3f, 0xa6, 0xb6, 0xc7, 0x5c, - 0x8a, 0x7e, 0x82, 0x7c, 0x54, 0x1b, 0xd6, 0x95, 0x45, 0x51, 0xcf, 0x1d, 0x5b, 0xcc, 0xa5, 0xcf, - 0x56, 0xfe, 0x02, 0x87, 0xe1, 0x7a, 0x61, 0x38, 0x22, 0xf1, 0x59, 0x09, 0xe1, 0x59, 0x3d, 0x7b, - 0x84, 0xc7, 0xd7, 0xb0, 0x03, 0x28, 0xce, 0x42, 0x31, 0x38, 0x45, 0x1b, 0x01, 0xaf, 0x56, 0xc5, - 0xcb, 0xb9, 0x13, 0xa1, 0x3d, 0x28, 0x08, 0xf9, 0x85, 0x5e, 0x0b, 0xd2, 0xc7, 0xa9, 0x59, 0x7d, - 0xf3, 0x1c, 0xfd, 0xe0, 0x26, 0x04, 0x55, 0xca, 0xed, 0x71, 0xee, 0xa5, 0xdc, 0x9e, 0xca, 0x37, - 0x1d, 0x4a, 0xa9, 0x57, 0x88, 0x2e, 0x9e, 0x79, 0x65, 0xdb, 0xfe, 0x6a, 0xcf, 0x0b, 0x22, 0xcf, - 0xdf, 0xbe, 0xfb, 0xb3, 0x39, 0xb5, 0xd9, 0x6c, 0x35, 0x6a, 0x8c, 0xdd, 0x65, 0x73, 0x11, 0xa6, - 0x80, 0x63, 0x3b, 0x53, 0x87, 0xb0, 0xb5, 0x4b, 0xe7, 0xcd, 0x85, 0x33, 0x69, 0xf2, 0x4c, 0x6c, - 0x6e, 0x8d, 0x46, 0x39, 0xfe, 0x5b, 0xf9, 0xf6, 0xff, 0x00, 0x00, 0x00, 0xff, 0xff, 0xbf, 0xc4, - 0xea, 0x93, 0x74, 0x07, 0x00, 0x00, + // 976 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x56, 0xed, 0x6e, 0xe2, 0x46, + 0x14, 0x2d, 0x21, 0x61, 0xc3, 0x05, 0x12, 0x67, 0x08, 0x89, 0x97, 0xcd, 0x6e, 0xa8, 0xfb, 0x21, + 0xd4, 0x56, 0xa0, 0x66, 0xdb, 0xaa, 0x6a, 0x7f, 0xb4, 0x59, 0x70, 0x44, 0xc4, 0x87, 0xa9, 0xed, + 0x6c, 0xba, 0x55, 0xa5, 0x91, 0x81, 0x59, 0xb0, 0x00, 0xdb, 0x3b, 0x1e, 0x0a, 0xfc, 0x6d, 0x9f, + 0xa4, 0xaf, 0xd1, 0xa7, 0xab, 0x3c, 0xb6, 0xc9, 0x18, 0x92, 0x4a, 0xfd, 0x15, 0xe7, 0x9c, 0x73, + 0xcf, 0xdc, 0xb9, 0x33, 0x73, 0x04, 0x3c, 0x5f, 0x5a, 0xb3, 0x19, 0x61, 0xd4, 0x1b, 0xd6, 0xc3, + 0xaf, 0xa9, 0xcd, 0x6a, 0x1e, 0x75, 0x99, 0x8b, 0xb2, 0x1b, 0xaa, 0x9c, 0xa5, 0xde, 0x30, 0x44, + 0xcb, 0xa7, 0xbe, 0x3d, 0x76, 0x02, 0x79, 0xf0, 0x97, 0xd0, 0x10, 0x55, 0x7e, 0x81, 0x4c, 0x9b, + 0xac, 0x75, 0xf2, 0x01, 0x55, 0x41, 0x9a, 0x92, 0x35, 0x7e, 0x6f, 0x3b, 0x63, 0x42, 0xb1, 0x47, + 0x6d, 0x87, 0xc9, 0xa9, 0x4a, 0xaa, 0x7a, 0xa0, 0x1f, 0x4d, 0xc9, 0xfa, 0x86, 0xc3, 0xfd, 0x00, + 0x45, 0x2f, 0x01, 0xb8, 0xd2, 0x9a, 0xdb, 0xb3, 0xb5, 0xbc, 0xc7, 0x35, 0xd9, 0x40, 0xc3, 0x01, + 0xa5, 0x00, 0xb9, 0xeb, 0xd1, 0x88, 0xea, 0xe4, 0xc3, 0x82, 0xf8, 0x4c, 0x51, 0x20, 0x1f, 0xfe, + 0xeb, 0x7b, 0xae, 0xe3, 0x13, 0x84, 0x60, 0xdf, 0x1a, 0x8d, 0x28, 0xf7, 0xce, 0xea, 0xfc, 0x5b, + 0xf9, 0x14, 0x72, 0x26, 0xb5, 0x1c, 0xdf, 0x1a, 0x32, 0xdb, 0x75, 0x50, 0x09, 0x32, 0x6c, 0x85, + 0x27, 0x64, 0xc5, 0x45, 0x79, 0xfd, 0x80, 0xad, 0x5a, 0x64, 0xa5, 0x7c, 0x07, 0xc7, 0xfd, 0xc5, + 0x60, 0x66, 0xfb, 0x93, 0x8d, 0xd9, 0x27, 0x50, 0xf0, 0x42, 0x08, 0x13, 0x4a, 0xdd, 0xd8, 0x35, + 0x1f, 0x81, 0x6a, 0x80, 0x29, 0xbf, 0x03, 0x32, 0x88, 0x33, 0xd2, 0x16, 0xcc, 0x5b, 0x30, 0x3f, + 0xea, 0x0b, 0x5d, 0x00, 0xf8, 0x16, 0xc3, 0x1e, 0xa1, 0x78, 0xba, 0xe4, 0x75, 0x69, 0xfd, 0xd0, + 0xb7, 0x58, 0x9f, 0xd0, 0xf6, 0x12, 0x55, 0xe1, 0x99, 0x1b, 0xea, 0xe5, 0xbd, 0x4a, 0xba, 0x9a, + 0xbb, 0x3a, 0xaa, 0x45, 0xf3, 0xab, 0x99, 0x2b, 0x6d, 0xc1, 0xf4, 0x98, 0x56, 0xbe, 0x82, 0x62, + 0xc2, 0x3d, 0xea, 0xac, 0x04, 0x19, 0x6a, 0x2d, 0x31, 0xdb, 0xec, 0x81, 0x5a, 0x4b, 0x73, 0xa5, + 0x7c, 0x0b, 0x48, 0xf5, 0x99, 0x3d, 0xb7, 0x18, 0xb9, 0x21, 0x24, 0xee, 0xe5, 0x12, 0x72, 0x43, + 0xd7, 0x79, 0x8f, 0x99, 0x45, 0xc7, 0x24, 0x1e, 0x3b, 0x04, 0x90, 0xc9, 0x11, 0xe5, 0x35, 0x14, + 0x13, 0x65, 0xd1, 0x22, 0xff, 0xb9, 0x07, 0xe5, 0xef, 0x3d, 0xc8, 0xf7, 0x89, 0x33, 0xb2, 0x9d, + 0xb1, 0xb1, 0x24, 0xc4, 0x43, 0x5f, 0xc2, 0x61, 0xd0, 0xb5, 0x1b, 0x1f, 0x6d, 0xee, 0xea, 0xb8, + 0x36, 0xe3, 0x7b, 0xd2, 0x16, 0xac, 0x1f, 0xc0, 0xfa, 0x46, 0x80, 0x7e, 0x80, 0xfc, 0xd2, 0x66, + 0x0e, 0xf1, 0x7d, 0xcc, 0xd6, 0x1e, 0xe1, 0xe7, 0x7c, 0x74, 0x75, 0x56, 0xdb, 0x5c, 0xae, 0xda, + 0x7d, 0x48, 0x9b, 0x6b, 0x8f, 0xe8, 0x09, 0x2d, 0x7a, 0x05, 0x60, 0xcd, 0xdd, 0x85, 0xc3, 0xb0, + 0x6f, 0x31, 0x39, 0x5d, 0x49, 0x55, 0x0b, 0xba, 0x80, 0x20, 0x05, 0xf2, 0x71, 0xdf, 0x83, 0x35, + 0x23, 0xf2, 0x3e, 0x57, 0x24, 0x30, 0x54, 0x03, 0x34, 0xa0, 0xae, 0x35, 0x1a, 0x5a, 0x3e, 0xc3, + 0x16, 0x63, 0x64, 0xee, 0x31, 0x5f, 0x3e, 0xe0, 0xca, 0x47, 0x18, 0xf4, 0x0d, 0x94, 0x1c, 0xb2, + 0x62, 0xf8, 0x81, 0x9a, 0x10, 0x7b, 0x3c, 0x61, 0x72, 0x86, 0x97, 0x3c, 0x4e, 0x2a, 0x67, 0x70, + 0x2a, 0x8e, 0x28, 0xbe, 0x1d, 0xca, 0xaf, 0x50, 0xda, 0xc2, 0xa3, 0x91, 0xff, 0x04, 0x47, 0x5e, + 0x48, 0x60, 0x9f, 0x33, 0x72, 0x8a, 0xdf, 0x8f, 0x73, 0x61, 0x30, 0x62, 0xa5, 0xbe, 0x25, 0x57, + 0xfe, 0x4a, 0xc1, 0xd1, 0x9b, 0xc5, 0xdc, 0x13, 0x8e, 0xff, 0x7f, 0x9d, 0x4b, 0x05, 0x72, 0xe1, + 0x35, 0xc1, 0xc1, 0xfd, 0xe0, 0xc7, 0x52, 0xd0, 0x45, 0x68, 0x67, 0xba, 0xe9, 0xdd, 0xe9, 0x2a, + 0x27, 0x70, 0xbc, 0x69, 0x22, 0xdc, 0xd9, 0x17, 0x7f, 0xa6, 0x21, 0x27, 0x1c, 0x29, 0x2a, 0xc2, + 0xf1, 0x5d, 0xaf, 0xdd, 0xd3, 0xee, 0x7b, 0xf8, 0xfe, 0xd6, 0xec, 0xa9, 0x86, 0x21, 0x7d, 0x84, + 0x64, 0x38, 0x6d, 0x68, 0xdd, 0xee, 0xad, 0xd9, 0x55, 0x7b, 0x26, 0x36, 0x6f, 0xbb, 0x2a, 0xee, + 0x68, 0x8d, 0xb6, 0x94, 0x42, 0xe7, 0x50, 0x14, 0x98, 0x9e, 0x86, 0x9b, 0x6a, 0xe7, 0xfa, 0x9d, + 0xb4, 0x87, 0x4a, 0x70, 0x22, 0x10, 0xba, 0xfa, 0x56, 0x6b, 0xab, 0x52, 0x3a, 0xd0, 0xb7, 0xcc, + 0x4e, 0x03, 0x6b, 0x37, 0x37, 0xaa, 0xae, 0x36, 0x63, 0x62, 0x3f, 0x58, 0x82, 0x13, 0xd7, 0x8d, + 0x86, 0xda, 0x37, 0x1f, 0x98, 0x03, 0xf4, 0x19, 0x7c, 0x9c, 0x28, 0x09, 0x96, 0xd7, 0xee, 0x4c, + 0x6c, 0xa8, 0x0d, 0xad, 0xd7, 0xc4, 0x1d, 0xf5, 0xad, 0xda, 0x91, 0x32, 0xe8, 0x73, 0x50, 0x92, + 0x06, 0xc6, 0x5d, 0xa3, 0xa1, 0x1a, 0x46, 0x52, 0xf7, 0x0c, 0x5d, 0xc2, 0x8b, 0xad, 0x0e, 0xba, + 0x9a, 0xa9, 0xc6, 0xae, 0xd2, 0x21, 0xaa, 0xc0, 0xc5, 0x76, 0x27, 0x5c, 0x11, 0xf9, 0x49, 0x59, + 0x74, 0x01, 0x32, 0x57, 0x88, 0xce, 0x71, 0xbf, 0x80, 0x4e, 0x41, 0x8a, 0x26, 0x87, 0xdb, 0xea, + 0x3b, 0xdc, 0xba, 0x36, 0x5a, 0x52, 0x0e, 0xbd, 0x80, 0xf3, 0x9e, 0x6a, 0x04, 0x76, 0x3b, 0x64, + 0xfe, 0xea, 0x9f, 0x7d, 0xc8, 0xde, 0xf3, 0x8b, 0xd4, 0xb6, 0x83, 0x37, 0x58, 0x68, 0x12, 0x6a, + 0xff, 0x41, 0x7a, 0x64, 0xc5, 0xda, 0x64, 0x8d, 0x4e, 0x84, 0x5b, 0x16, 0xe6, 0x76, 0xf9, 0x6c, + 0x13, 0x4c, 0x6d, 0xb2, 0x6e, 0x12, 0x7f, 0x48, 0x6d, 0x8f, 0xb9, 0x14, 0x7d, 0x0f, 0xd9, 0xb0, + 0x36, 0xa8, 0x2b, 0x8a, 0xa2, 0x8e, 0x3b, 0xb4, 0x98, 0x4b, 0x9f, 0xac, 0xfc, 0x11, 0x0e, 0x83, + 0xf5, 0x82, 0xd4, 0x46, 0xe2, 0x7b, 0x17, 0x52, 0xbd, 0x7c, 0xbe, 0x83, 0x47, 0xef, 0xa3, 0x05, + 0x28, 0x0a, 0x69, 0x31, 0xd1, 0x45, 0x1b, 0x01, 0x2f, 0x97, 0xc5, 0x57, 0xb3, 0x95, 0xed, 0x1d, + 0xc8, 0x09, 0xc1, 0x8a, 0x5e, 0x0a, 0xd2, 0xdd, 0x38, 0x2f, 0xbf, 0x7a, 0x8a, 0x7e, 0x70, 0x13, + 0x12, 0x34, 0xe1, 0xb6, 0x1b, 0xc8, 0x09, 0xb7, 0xc7, 0x82, 0x57, 0x87, 0x42, 0x22, 0x1e, 0xd0, + 0xe5, 0x13, 0xcf, 0x7f, 0xd3, 0x5f, 0xe5, 0x69, 0x41, 0xe4, 0xf9, 0x33, 0x3c, 0x8b, 0x9e, 0x24, + 0x7a, 0x2e, 0x88, 0x93, 0x59, 0x91, 0x98, 0xd8, 0xd6, 0x0b, 0x7e, 0xf3, 0xf5, 0x6f, 0xf5, 0xb1, + 0xcd, 0x26, 0x8b, 0x41, 0x6d, 0xe8, 0xce, 0xeb, 0xb3, 0x20, 0xe0, 0x1c, 0xdb, 0x19, 0x3b, 0x84, + 0x2d, 0x5d, 0x3a, 0xad, 0xcf, 0x9c, 0x51, 0x9d, 0xc7, 0x4a, 0x7d, 0x63, 0x31, 0xc8, 0xf0, 0x9f, + 0x01, 0xaf, 0xff, 0x0d, 0x00, 0x00, 0xff, 0xff, 0xd3, 0xc7, 0x77, 0x11, 0x4f, 0x08, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -806,6 +902,33 @@ type WalletKitClient interface { //remain supported. This is an advanced API that depends on the internals of //the UtxoSweeper, so things may change. PendingSweeps(ctx context.Context, in *PendingSweepsRequest, opts ...grpc.CallOption) (*PendingSweepsResponse, error) + // + //Bump the fee of an arbitrary input within a transaction. This RPC takes a + //different approach than bitcoind's bumpfee command. lnd has a central + //batching engine in which inputs with similar fee rates are batched together + //to save on transaction fees. Due to this, we cannot rely on bumping the fee + //on a specific transaction, since transactions can change at any point with + //the addition of new inputs. The list of inputs that currently exist within + //lnd's central batching engine can be retrieved through the PendingSweeps + //RPC. + // + //When bumping the fee of an input that currently exists within lnd's central + //batching engine, a higher fee transaction will be created that replaces the + //lower fee transaction through the Replace-By-Fee (RBF) policy. If it + // + //This RPC also serves useful when wanting to perform a Child-Pays-For-Parent + //(CPFP), where the child transaction pays for its parent's fee. This can be + //done by specifying an outpoint within the low fee transaction that is under + //the control of the wallet. + // + //The fee preference can be expressed either as a specific fee rate or a delta + //of blocks in which the output should be swept on-chain within. If a fee + //preference is not explicitly specified, then an error is returned. + // + //Note that this RPC currently doesn't perform any validation checks on the + //fee preference being provided. For now, the responsibility of ensuring that + //the new fee preference is sufficient is delegated to the user. + BumpFee(ctx context.Context, in *BumpFeeRequest, opts ...grpc.CallOption) (*BumpFeeResponse, error) } type walletKitClient struct { @@ -879,6 +1002,15 @@ func (c *walletKitClient) PendingSweeps(ctx context.Context, in *PendingSweepsRe return out, nil } +func (c *walletKitClient) BumpFee(ctx context.Context, in *BumpFeeRequest, opts ...grpc.CallOption) (*BumpFeeResponse, error) { + out := new(BumpFeeResponse) + err := c.cc.Invoke(ctx, "/walletrpc.WalletKit/BumpFee", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // WalletKitServer is the server API for WalletKit service. type WalletKitServer interface { //* @@ -919,6 +1051,33 @@ type WalletKitServer interface { //remain supported. This is an advanced API that depends on the internals of //the UtxoSweeper, so things may change. PendingSweeps(context.Context, *PendingSweepsRequest) (*PendingSweepsResponse, error) + // + //Bump the fee of an arbitrary input within a transaction. This RPC takes a + //different approach than bitcoind's bumpfee command. lnd has a central + //batching engine in which inputs with similar fee rates are batched together + //to save on transaction fees. Due to this, we cannot rely on bumping the fee + //on a specific transaction, since transactions can change at any point with + //the addition of new inputs. The list of inputs that currently exist within + //lnd's central batching engine can be retrieved through the PendingSweeps + //RPC. + // + //When bumping the fee of an input that currently exists within lnd's central + //batching engine, a higher fee transaction will be created that replaces the + //lower fee transaction through the Replace-By-Fee (RBF) policy. If it + // + //This RPC also serves useful when wanting to perform a Child-Pays-For-Parent + //(CPFP), where the child transaction pays for its parent's fee. This can be + //done by specifying an outpoint within the low fee transaction that is under + //the control of the wallet. + // + //The fee preference can be expressed either as a specific fee rate or a delta + //of blocks in which the output should be swept on-chain within. If a fee + //preference is not explicitly specified, then an error is returned. + // + //Note that this RPC currently doesn't perform any validation checks on the + //fee preference being provided. For now, the responsibility of ensuring that + //the new fee preference is sufficient is delegated to the user. + BumpFee(context.Context, *BumpFeeRequest) (*BumpFeeResponse, error) } func RegisterWalletKitServer(s *grpc.Server, srv WalletKitServer) { @@ -1051,6 +1210,24 @@ func _WalletKit_PendingSweeps_Handler(srv interface{}, ctx context.Context, dec return interceptor(ctx, in, info, handler) } +func _WalletKit_BumpFee_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(BumpFeeRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WalletKitServer).BumpFee(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/walletrpc.WalletKit/BumpFee", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WalletKitServer).BumpFee(ctx, req.(*BumpFeeRequest)) + } + return interceptor(ctx, in, info, handler) +} + var _WalletKit_serviceDesc = grpc.ServiceDesc{ ServiceName: "walletrpc.WalletKit", HandlerType: (*WalletKitServer)(nil), @@ -1083,6 +1260,10 @@ var _WalletKit_serviceDesc = grpc.ServiceDesc{ MethodName: "PendingSweeps", Handler: _WalletKit_PendingSweeps_Handler, }, + { + MethodName: "BumpFee", + Handler: _WalletKit_BumpFee_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "walletrpc/walletkit.proto", diff --git a/lnrpc/walletrpc/walletkit.proto b/lnrpc/walletrpc/walletkit.proto index eba003442..0bc39d707 100644 --- a/lnrpc/walletrpc/walletkit.proto +++ b/lnrpc/walletrpc/walletkit.proto @@ -203,6 +203,23 @@ message PendingSweepsResponse { repeated PendingSweep pending_sweeps = 1 [json_name = "pending_sweeps"]; } +message BumpFeeRequest { + // The input we're attempting to bump the fee of. + lnrpc.OutPoint outpoint = 1 [json_name = "outpoint"]; + + // The target number of blocks that the input should be spent within. + uint32 target_conf = 2 [json_name = "target_conf"]; + + /* + The fee rate, expressed in sat/byte, that should be used to spend the input + with. + */ + uint32 sat_per_byte = 3 [json_name = "sat_per_byte"]; +} + +message BumpFeeResponse { +} + service WalletKit { /** DeriveNextKey attempts to derive the *next* key within the key family @@ -255,4 +272,33 @@ service WalletKit { the UtxoSweeper, so things may change. */ rpc PendingSweeps(PendingSweepsRequest) returns (PendingSweepsResponse); + + /* + Bump the fee of an arbitrary input within a transaction. This RPC takes a + different approach than bitcoind's bumpfee command. lnd has a central + batching engine in which inputs with similar fee rates are batched together + to save on transaction fees. Due to this, we cannot rely on bumping the fee + on a specific transaction, since transactions can change at any point with + the addition of new inputs. The list of inputs that currently exist within + lnd's central batching engine can be retrieved through the PendingSweeps + RPC. + + When bumping the fee of an input that currently exists within lnd's central + batching engine, a higher fee transaction will be created that replaces the + lower fee transaction through the Replace-By-Fee (RBF) policy. If it + + This RPC also serves useful when wanting to perform a Child-Pays-For-Parent + (CPFP), where the child transaction pays for its parent's fee. This can be + done by specifying an outpoint within the low fee transaction that is under + the control of the wallet. + + The fee preference can be expressed either as a specific fee rate or a delta + of blocks in which the output should be swept on-chain within. If a fee + preference is not explicitly specified, then an error is returned. + + Note that this RPC currently doesn't perform any validation checks on the + fee preference being provided. For now, the responsibility of ensuring that + the new fee preference is sufficient is delegated to the user. + */ + rpc BumpFee(BumpFeeRequest) returns (BumpFeeResponse); } diff --git a/lnrpc/walletrpc/walletkit_server.go b/lnrpc/walletrpc/walletkit_server.go index f2e04f593..99100a7f4 100644 --- a/lnrpc/walletrpc/walletkit_server.go +++ b/lnrpc/walletrpc/walletkit_server.go @@ -9,12 +9,15 @@ import ( "os" "path/filepath" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/signrpc" "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/sweep" "golang.org/x/net/context" "google.golang.org/grpc" "gopkg.in/macaroon-bakery.v2/bakery" @@ -79,6 +82,10 @@ var ( Entity: "onchain", Action: "read", }}, + "/walletrpc.WalletKit/BumpFee": {{ + Entity: "onchain", + Action: "write", + }}, } // DefaultWalletKitMacFilename is the default name of the wallet kit @@ -409,3 +416,106 @@ func (w *WalletKit) PendingSweeps(ctx context.Context, PendingSweeps: rpcPendingSweeps, }, nil } + +// unmarshallOutPoint converts an outpoint from its lnrpc type to its canonical +// type. +func unmarshallOutPoint(op *lnrpc.OutPoint) (*wire.OutPoint, error) { + var hash chainhash.Hash + switch { + case len(op.TxidBytes) == 0 && len(op.TxidStr) == 0: + fallthrough + + case len(op.TxidBytes) != 0 && len(op.TxidStr) != 0: + return nil, fmt.Errorf("either TxidBytes or TxidStr must be " + + "specified, but not both") + + // The hash was provided as raw bytes. + case len(op.TxidBytes) != 0: + copy(hash[:], op.TxidBytes) + + // The hash was provided as a hex-encoded string. + case len(op.TxidStr) != 0: + h, err := chainhash.NewHashFromStr(op.TxidStr) + if err != nil { + return nil, err + } + hash = *h + } + + return &wire.OutPoint{ + Hash: hash, + Index: op.OutputIndex, + }, nil +} + +// BumpFee allows bumping the fee rate of an arbitrary input. A fee preference +// can be expressed either as a specific fee rate or a delta of blocks in which +// the output should be swept on-chain within. If a fee preference is not +// explicitly specified, then an error is returned. The status of the input +// sweep can be checked through the PendingSweeps RPC. +func (w *WalletKit) BumpFee(ctx context.Context, + in *BumpFeeRequest) (*BumpFeeResponse, error) { + + // Parse the outpoint from the request. + op, err := unmarshallOutPoint(in.Outpoint) + if err != nil { + return nil, err + } + + // Construct the request's fee preference. + satPerKw := lnwallet.SatPerKVByte(in.SatPerByte * 1000).FeePerKWeight() + feePreference := sweep.FeePreference{ + ConfTarget: uint32(in.TargetConf), + FeeRate: satPerKw, + } + + // We'll attempt to bump the fee of the input through the UtxoSweeper. + // If it is currently attempting to sweep the input, then it'll simply + // bump its fee, which will result in a replacement transaction (RBF) + // being broadcast. If it is not aware of the input however, + // lnwallet.ErrNotMine is returned. + _, err = w.cfg.Sweeper.BumpFee(*op, feePreference) + switch err { + case nil: + return &BumpFeeResponse{}, nil + case lnwallet.ErrNotMine: + break + default: + return nil, err + } + + // Since we're unable to perform a bump through RBF, we'll assume the + // user is attempting to bump an unconfirmed transaction's fee rate by + // sweeping an output within it under control of the wallet with a + // higher fee rate, essentially performing a Child-Pays-For-Parent + // (CPFP). + // + // We'll gather all of the information required by the UtxoSweeper in + // order to sweep the output. + txOut, err := w.cfg.Wallet.FetchInputInfo(op) + if err != nil { + return nil, err + } + + var witnessType input.WitnessType + switch { + case txscript.IsPayToWitnessPubKeyHash(txOut.PkScript): + witnessType = input.WitnessKeyHash + case txscript.IsPayToScriptHash(txOut.PkScript): + witnessType = input.NestedWitnessKeyHash + default: + return nil, fmt.Errorf("unknown input witness %v", op) + } + + signDesc := &input.SignDescriptor{ + Output: txOut, + HashType: txscript.SigHashAll, + } + + input := input.NewBaseInput(op, witnessType, signDesc, 0) + if _, err = w.cfg.Sweeper.SweepInput(input, feePreference); err != nil { + return nil, err + } + + return &BumpFeeResponse{}, nil +} From d41af9a65feb7609d858814085f24f79dbb3b2b6 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Wed, 29 May 2019 14:00:41 -0700 Subject: [PATCH 5/6] cmd/lncli: expose bumping fee of inputs/transactions over lncli --- cmd/lncli/types.go | 25 ++++++++++ cmd/lncli/walletrpc_active.go | 88 +++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+) diff --git a/cmd/lncli/types.go b/cmd/lncli/types.go index 78d640f94..30d559a7e 100644 --- a/cmd/lncli/types.go +++ b/cmd/lncli/types.go @@ -1,7 +1,11 @@ package main import ( + "encoding/hex" + "errors" "fmt" + "strconv" + "strings" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/lightningnetwork/lnd/lnrpc" @@ -17,6 +21,27 @@ func NewOutPointFromProto(op *lnrpc.OutPoint) OutPoint { return OutPoint(fmt.Sprintf("%v:%d", hash, op.OutputIndex)) } +// NewProtoOutPoint parses an OutPoint into its corresponding lnrpc.OutPoint +// type. +func NewProtoOutPoint(op string) (*lnrpc.OutPoint, error) { + parts := strings.Split(op, ":") + if len(parts) != 2 { + return nil, errors.New("outpoint should be of the form txid:index") + } + txid := parts[0] + if hex.DecodedLen(len(txid)) != chainhash.HashSize { + return nil, fmt.Errorf("invalid hex-encoded txid %v", txid) + } + outputIndex, err := strconv.Atoi(parts[1]) + if err != nil { + return nil, fmt.Errorf("invalid output index: %v", err) + } + return &lnrpc.OutPoint{ + TxidStr: txid, + OutputIndex: uint32(outputIndex), + }, nil +} + // Utxo displays information about an unspent output, including its address, // amount, pkscript, and confirmations. type Utxo struct { diff --git a/cmd/lncli/walletrpc_active.go b/cmd/lncli/walletrpc_active.go index e86819bb4..35dc562d6 100644 --- a/cmd/lncli/walletrpc_active.go +++ b/cmd/lncli/walletrpc_active.go @@ -4,6 +4,7 @@ package main import ( "context" + "fmt" "sort" "github.com/lightningnetwork/lnd/lnrpc/walletrpc" @@ -21,6 +22,7 @@ func walletCommands() []cli.Command { Description: "", Subcommands: []cli.Command{ pendingSweepsCommand, + bumpFeeCommand, }, }, } @@ -81,3 +83,89 @@ func pendingSweeps(ctx *cli.Context) error { return nil } + +var bumpFeeCommand = cli.Command{ + Name: "bumpfee", + Usage: "Bumps the fee of an arbitrary input/transaction.", + ArgsUsage: "outpoint", + Description: ` + This command takes a different approach than bitcoind's bumpfee command. + lnd has a central batching engine in which inputs with similar fee rates + are batched together to save on transaction fees. Due to this, we cannot + rely on bumping the fee on a specific transaction, since transactions + can change at any point with the addition of new inputs. The list of + inputs that currently exist within lnd's central batching engine can be + retrieved through lncli pendingsweeps. + + When bumping the fee of an input that currently exists within lnd's + central batching engine, a higher fee transaction will be created that + replaces the lower fee transaction through the Replace-By-Fee (RBF) + policy. + + This command also serves useful when wanting to perform a + Child-Pays-For-Parent (CPFP), where the child transaction pays for its + parent's fee. This can be done by specifying an outpoint within the low + fee transaction that is under the control of the wallet. + + A fee preference must be provided, either through the conf_target or + sat_per_byte parameters. + + Note that this command currently doesn't perform any validation checks + on the fee preference being provided. For now, the responsibility of + ensuring that the new fee preference is sufficient is delegated to the + user.`, + Flags: []cli.Flag{ + cli.Uint64Flag{ + Name: "conf_target", + Usage: "the number of blocks that the output should " + + "be swept on-chain within", + }, + cli.Uint64Flag{ + Name: "sat_per_byte", + Usage: "a manual fee expressed in sat/byte that " + + "should be used when sweeping the output", + }, + }, + Action: actionDecorator(bumpFee), +} + +func bumpFee(ctx *cli.Context) error { + // Display the command's help message if we do not have the expected + // number of arguments/flags. + if ctx.NArg() != 1 || ctx.NumFlags() != 1 { + return cli.ShowCommandHelp(ctx, "bumpfee") + } + + // Validate and parse the relevant arguments/flags. + protoOutPoint, err := NewProtoOutPoint(ctx.Args().Get(0)) + if err != nil { + return err + } + + var confTarget, satPerByte uint32 + switch { + case ctx.IsSet("conf_target") && ctx.IsSet("sat_per_byte"): + return fmt.Errorf("either conf_target or sat_per_byte should " + + "be set, but not both") + case ctx.IsSet("conf_target"): + confTarget = uint32(ctx.Uint64("conf_target")) + case ctx.IsSet("sat_per_byte"): + satPerByte = uint32(ctx.Uint64("sat_per_byte")) + } + + client, cleanUp := getWalletClient(ctx) + defer cleanUp() + + resp, err := client.BumpFee(context.Background(), &walletrpc.BumpFeeRequest{ + Outpoint: protoOutPoint, + TargetConf: confTarget, + SatPerByte: satPerByte, + }) + if err != nil { + return err + } + + printRespJSON(resp) + + return nil +} From 638355b60301cb7c874a436d2571df5536a6348f Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Mon, 3 Jun 2019 16:12:20 -0700 Subject: [PATCH 6/6] lntest: add CPFP integration test --- lntest/itest/lnd_test.go | 4 + lntest/itest/onchain.go | 160 +++++++++++++++++++++++++++++++++++++++ lntest/node.go | 18 +++-- 3 files changed, 174 insertions(+), 8 deletions(-) create mode 100644 lntest/itest/onchain.go diff --git a/lntest/itest/lnd_test.go b/lntest/itest/lnd_test.go index e318df063..0fc29d72b 100644 --- a/lntest/itest/lnd_test.go +++ b/lntest/itest/lnd_test.go @@ -13733,6 +13733,10 @@ var testsCases = []*testCase{ name: "hold invoice sender persistence", test: testHoldInvoicePersistence, }, + { + name: "cpfp", + test: testCPFP, + }, } // TestLightningNetworkDaemon performs a series of integration tests amongst a diff --git a/lntest/itest/onchain.go b/lntest/itest/onchain.go new file mode 100644 index 000000000..2d10db52d --- /dev/null +++ b/lntest/itest/onchain.go @@ -0,0 +1,160 @@ +// +build rpctest + +package itest + +import ( + "bytes" + "context" + "fmt" + + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcutil" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/walletrpc" + "github.com/lightningnetwork/lnd/lntest" + "github.com/lightningnetwork/lnd/sweep" +) + +// testCPFP ensures that the daemon can bump an unconfirmed transaction's fee +// rate by broadcasting a Child-Pays-For-Parent (CPFP) transaction. +// +// TODO(wilmer): Add RBF case once btcd supports it. +func testCPFP(net *lntest.NetworkHarness, t *harnessTest) { + // Skip this test for neutrino, as it's not aware of mempool + // transactions. + if net.BackendCfg.Name() == "neutrino" { + t.Skipf("skipping reorg test for neutrino backend") + } + + // We'll start the test by sending Alice some coins, which she'll use to + // send to Bob. + ctxb := context.Background() + ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) + err := net.SendCoins(ctxt, btcutil.SatoshiPerBitcoin, net.Alice) + if err != nil { + t.Fatalf("unable to send coins to alice: %v", err) + } + + // Create an address for Bob to send the coins to. + addrReq := &lnrpc.NewAddressRequest{ + Type: lnrpc.AddressType_WITNESS_PUBKEY_HASH, + } + ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) + resp, err := net.Bob.NewAddress(ctxt, addrReq) + if err != nil { + t.Fatalf("unable to get new address for bob: %v", err) + } + + // Send the coins from Alice to Bob. We should expect a transaction to + // be broadcast and seen in the mempool. + sendReq := &lnrpc.SendCoinsRequest{ + Addr: resp.Address, + Amount: btcutil.SatoshiPerBitcoin, + } + ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) + if _, err = net.Alice.SendCoins(ctxt, sendReq); err != nil { + t.Fatalf("unable to send coins to bob: %v", err) + } + + txid, err := waitForTxInMempool(net.Miner.Node, minerMempoolTimeout) + if err != nil { + t.Fatalf("expected one mempool transaction: %v", err) + } + + // We'll then extract the raw transaction from the mempool in order to + // determine the index of Bob's output. + tx, err := net.Miner.Node.GetRawTransaction(txid) + if err != nil { + t.Fatalf("unable to extract raw transaction from mempool: %v", + err) + } + bobOutputIdx := -1 + for i, txOut := range tx.MsgTx().TxOut { + _, addrs, _, err := txscript.ExtractPkScriptAddrs( + txOut.PkScript, net.Miner.ActiveNet, + ) + if err != nil { + t.Fatalf("unable to extract address from pkScript=%x: "+ + "%v", txOut.PkScript, err) + } + if addrs[0].String() == resp.Address { + bobOutputIdx = i + } + } + if bobOutputIdx == -1 { + t.Fatalf("bob's output was not found within the transaction") + } + + // We'll attempt to bump the fee of this transaction by performing a + // CPFP from Alice's point of view. + op := &lnrpc.OutPoint{ + TxidBytes: txid[:], + OutputIndex: uint32(bobOutputIdx), + } + bumpFeeReq := &walletrpc.BumpFeeRequest{ + Outpoint: op, + SatPerByte: uint32(sweep.DefaultMaxFeeRate.FeePerKVByte() / 1000), + } + ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) + _, err = net.Bob.WalletKitClient.BumpFee(ctxt, bumpFeeReq) + if err != nil { + t.Fatalf("unable to bump fee: %v", err) + } + + // We should now expect to see two transactions within the mempool, a + // parent and its child. + _, err = waitForNTxsInMempool(net.Miner.Node, 2, minerMempoolTimeout) + if err != nil { + t.Fatalf("expected two mempool transactions: %v", err) + } + + // We should also expect to see the output being swept by the + // UtxoSweeper. We'll ensure it's using the fee rate specified. + pendingSweepsReq := &walletrpc.PendingSweepsRequest{} + ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) + pendingSweepsResp, err := net.Bob.WalletKitClient.PendingSweeps( + ctxt, pendingSweepsReq, + ) + if err != nil { + t.Fatalf("unable to retrieve pending sweeps: %v", err) + } + if len(pendingSweepsResp.PendingSweeps) != 1 { + t.Fatalf("expected to find %v pending sweep(s), found %v", 1, + len(pendingSweepsResp.PendingSweeps)) + } + pendingSweep := pendingSweepsResp.PendingSweeps[0] + if !bytes.Equal(pendingSweep.Outpoint.TxidBytes, op.TxidBytes) { + t.Fatalf("expected output txid %x, got %x", op.TxidBytes, + pendingSweep.Outpoint.TxidBytes) + } + if pendingSweep.Outpoint.OutputIndex != op.OutputIndex { + t.Fatalf("expected output index %v, got %v", op.OutputIndex, + pendingSweep.Outpoint.OutputIndex) + } + if pendingSweep.SatPerByte != bumpFeeReq.SatPerByte { + t.Fatalf("expected sweep sat per byte %v, got %v", + bumpFeeReq.SatPerByte, pendingSweep.SatPerByte) + } + + // Mine a block to clean up the unconfirmed transactions. + mineBlocks(t, net, 1, 2) + + // The input used to CPFP should no longer be pending. + err = lntest.WaitNoError(func() error { + req := &walletrpc.PendingSweepsRequest{} + ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) + resp, err := net.Bob.WalletKitClient.PendingSweeps(ctxt, req) + if err != nil { + return fmt.Errorf("unable to retrieve bob's pending "+ + "sweeps: %v", err) + } + if len(resp.PendingSweeps) != 0 { + return fmt.Errorf("expected 0 pending sweeps, found %d", + len(resp.PendingSweeps)) + } + return nil + }, defaultTimeout) + if err != nil { + t.Fatalf(err.Error()) + } +} diff --git a/lntest/node.go b/lntest/node.go index fd63afb5b..79ec220f6 100644 --- a/lntest/node.go +++ b/lntest/node.go @@ -15,11 +15,6 @@ import ( "sync" "time" - "golang.org/x/net/context" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials" - macaroon "gopkg.in/macaroon.v2" - "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" @@ -29,7 +24,12 @@ 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/macaroons" + "golang.org/x/net/context" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + macaroon "gopkg.in/macaroon.v2" ) var ( @@ -250,9 +250,10 @@ type HarnessNode struct { invoicesrpc.InvoicesClient - // RouterClient cannot be embedded, because a name collision would occur - // on the main rpc SendPayment. - RouterClient routerrpc.RouterClient + // RouterClient and WalletKitClient cannot be embedded, because a name + // collision would occur with LightningClient. + RouterClient routerrpc.RouterClient + WalletKitClient walletrpc.WalletKitClient } // Assert *HarnessNode implements the lnrpc.LightningClient interface. @@ -503,6 +504,7 @@ func (hn *HarnessNode) initLightningClient(conn *grpc.ClientConn) error { hn.LightningClient = lnrpc.NewLightningClient(conn) hn.InvoicesClient = invoicesrpc.NewInvoicesClient(conn) hn.RouterClient = routerrpc.NewRouterClient(conn) + hn.WalletKitClient = walletrpc.NewWalletKitClient(conn) // Set the harness node's pubkey to what the node claims in GetInfo. err := hn.FetchNodeInfo()