diff --git a/cmd/lncli/main.go b/cmd/lncli/main.go index 73a6ce757..51917e4e5 100644 --- a/cmd/lncli/main.go +++ b/cmd/lncli/main.go @@ -304,6 +304,7 @@ func main() { app.Commands = append(app.Commands, autopilotCommands()...) app.Commands = append(app.Commands, invoicesCommands()...) app.Commands = append(app.Commands, routerCommands()...) + app.Commands = append(app.Commands, walletCommands()...) if err := app.Run(os.Args); err != nil { fatal(err) diff --git a/cmd/lncli/types.go b/cmd/lncli/types.go index 80f7cdf68..78d640f94 100644 --- a/cmd/lncli/types.go +++ b/cmd/lncli/types.go @@ -3,6 +3,7 @@ package main import ( "fmt" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/lightningnetwork/lnd/lnrpc" ) @@ -11,7 +12,9 @@ type OutPoint string // NewOutPointFromProto formats the lnrpc.OutPoint into an OutPoint for display. func NewOutPointFromProto(op *lnrpc.OutPoint) OutPoint { - return OutPoint(fmt.Sprintf("%s:%d", op.TxidStr, op.OutputIndex)) + var hash chainhash.Hash + copy(hash[:], op.TxidBytes) + return OutPoint(fmt.Sprintf("%v:%d", hash, op.OutputIndex)) } // Utxo displays information about an unspent output, including its address, diff --git a/cmd/lncli/walletrpc_active.go b/cmd/lncli/walletrpc_active.go new file mode 100644 index 000000000..e86819bb4 --- /dev/null +++ b/cmd/lncli/walletrpc_active.go @@ -0,0 +1,83 @@ +// +build walletrpc + +package main + +import ( + "context" + "sort" + + "github.com/lightningnetwork/lnd/lnrpc/walletrpc" + "github.com/urfave/cli" +) + +// walletCommands will return the set of commands to enable for walletrpc +// builds. +func walletCommands() []cli.Command { + return []cli.Command{ + { + Name: "wallet", + Category: "Wallet", + Usage: "Interact with the wallet.", + Description: "", + Subcommands: []cli.Command{ + pendingSweepsCommand, + }, + }, + } +} + +func getWalletClient(ctx *cli.Context) (walletrpc.WalletKitClient, func()) { + conn := getClientConn(ctx, false) + cleanUp := func() { + conn.Close() + } + return walletrpc.NewWalletKitClient(conn), cleanUp +} + +var pendingSweepsCommand = cli.Command{ + Name: "pendingsweeps", + Usage: "List all outputs that are pending to be swept within lnd.", + ArgsUsage: "", + Description: ` + List all on-chain outputs that lnd is currently attempting to sweep + within its central batching engine. Outputs with similar fee rates are + batched together in order to sweep them within a single transaction. + `, + Flags: []cli.Flag{}, + Action: actionDecorator(pendingSweeps), +} + +func pendingSweeps(ctx *cli.Context) error { + ctxb := context.Background() + client, cleanUp := getWalletClient(ctx) + defer cleanUp() + + req := &walletrpc.PendingSweepsRequest{} + resp, err := client.PendingSweeps(ctxb, req) + if err != nil { + return err + } + + // Sort them in ascending fee rate order for display purposes. + sort.Slice(resp.PendingSweeps, func(i, j int) bool { + return resp.PendingSweeps[i].SatPerByte < + resp.PendingSweeps[j].SatPerByte + }) + + var pendingSweepsResp = struct { + PendingSweeps []*PendingSweep `json:"pending_sweeps"` + }{ + PendingSweeps: make([]*PendingSweep, 0, len(resp.PendingSweeps)), + } + + for _, protoPendingSweep := range resp.PendingSweeps { + pendingSweep := NewPendingSweepFromProto(protoPendingSweep) + pendingSweepsResp.PendingSweeps = append( + pendingSweepsResp.PendingSweeps, pendingSweep, + ) + } + + printJSON(pendingSweepsResp) + + return nil +} diff --git a/cmd/lncli/walletrpc_default.go b/cmd/lncli/walletrpc_default.go new file mode 100644 index 000000000..f919a9933 --- /dev/null +++ b/cmd/lncli/walletrpc_default.go @@ -0,0 +1,10 @@ +// +build !walletrpc + +package main + +import "github.com/urfave/cli" + +// walletCommands will return nil for non-walletrpc builds. +func walletCommands() []cli.Command { + return nil +} diff --git a/cmd/lncli/walletrpc_types.go b/cmd/lncli/walletrpc_types.go new file mode 100644 index 000000000..f336e02e2 --- /dev/null +++ b/cmd/lncli/walletrpc_types.go @@ -0,0 +1,27 @@ +package main + +import "github.com/lightningnetwork/lnd/lnrpc/walletrpc" + +// PendingSweep is a CLI-friendly type of the walletrpc.PendingSweep proto. We +// use this to show more useful string versions of byte slices and enums. +type PendingSweep struct { + OutPoint OutPoint `json:"outpoint"` + WitnessType string `json:"witness_type"` + AmountSat uint32 `json:"amount_sat"` + SatPerByte uint32 `json:"sat_per_byte"` + BroadcastAttempts uint32 `json:"broadcast_attempts"` + NextBroadcastHeight uint32 `json:"next_broadcast_height"` +} + +// NewPendingSweepFromProto converts the walletrpc.PendingSweep proto type into +// its corresponding CLI-friendly type. +func NewPendingSweepFromProto(pendingSweep *walletrpc.PendingSweep) *PendingSweep { + return &PendingSweep{ + OutPoint: NewOutPointFromProto(pendingSweep.Outpoint), + WitnessType: pendingSweep.WitnessType.String(), + AmountSat: pendingSweep.AmountSat, + SatPerByte: pendingSweep.SatPerByte, + BroadcastAttempts: pendingSweep.BroadcastAttempts, + NextBroadcastHeight: pendingSweep.NextBroadcastHeight, + } +} diff --git a/lnrpc/walletrpc/config_active.go b/lnrpc/walletrpc/config_active.go index 6f7a207ee..77dc0e574 100644 --- a/lnrpc/walletrpc/config_active.go +++ b/lnrpc/walletrpc/config_active.go @@ -6,6 +6,7 @@ import ( "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/macaroons" + "github.com/lightningnetwork/lnd/sweep" ) // Config is the primary configuration struct for the WalletKit RPC server. It @@ -38,4 +39,8 @@ type Config struct { // KeyRing is an interface that the WalletKit will use to derive any // keys due to incoming client requests. KeyRing keychain.KeyRing + + // Sweeper is the central batching engine of lnd. It is responsible for + // sweeping inputs in batches back into the wallet. + Sweeper *sweep.UtxoSweeper } diff --git a/lnrpc/walletrpc/walletkit.pb.go b/lnrpc/walletrpc/walletkit.pb.go index 60a4a9a9f..ffdb00bc5 100644 --- a/lnrpc/walletrpc/walletkit.pb.go +++ b/lnrpc/walletrpc/walletkit.pb.go @@ -7,6 +7,7 @@ import ( context "context" fmt "fmt" proto "github.com/golang/protobuf/proto" + lnrpc "github.com/lightningnetwork/lnd/lnrpc" signrpc "github.com/lightningnetwork/lnd/lnrpc/signrpc" grpc "google.golang.org/grpc" math "math" @@ -23,6 +24,108 @@ var _ = math.Inf // proto package needs to be updated. const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package +type WitnessType int32 + +const ( + WitnessType_UNKNOWN_WITNESS WitnessType = 0 + // + //A witness that allows us to spend the output of a commitment transaction + //after a relative lock-time lockout. + WitnessType_COMMITMENT_TIME_LOCK WitnessType = 1 + // + //A witness that allows us to spend a settled no-delay output immediately on a + //counterparty's commitment transaction. + WitnessType_COMMITMENT_NO_DELAY WitnessType = 2 + // + //A witness that allows us to sweep the settled output of a malicious + //counterparty's who broadcasts a revoked commitment transaction. + WitnessType_COMMITMENT_REVOKE WitnessType = 3 + // + //A witness that allows us to sweep an HTLC which we offered to the remote + //party in the case that they broadcast a revoked commitment state. + WitnessType_HTLC_OFFERED_REVOKE WitnessType = 4 + // + //A witness that allows us to sweep an HTLC output sent to us in the case that + //the remote party broadcasts a revoked commitment state. + WitnessType_HTLC_ACCEPTED_REVOKE WitnessType = 5 + // + //A witness that allows us to sweep an HTLC output that we extended to a + //party, but was never fulfilled. This HTLC output isn't directly on the + //commitment transaction, but is the result of a confirmed second-level HTLC + //transaction. As a result, we can only spend this after a CSV delay. + WitnessType_HTLC_OFFERED_TIMEOUT_SECOND_LEVEL WitnessType = 6 + // + //A witness that allows us to sweep an HTLC output that was offered to us, and + //for which we have a payment preimage. This HTLC output isn't directly on our + //commitment transaction, but is the result of confirmed second-level HTLC + //transaction. As a result, we can only spend this after a CSV delay. + WitnessType_HTLC_ACCEPTED_SUCCESS_SECOND_LEVEL WitnessType = 7 + // + //A witness that allows us to sweep an HTLC that we offered to the remote + //party which lies in the commitment transaction of the remote party. We can + //spend this output after the absolute CLTV timeout of the HTLC as passed. + WitnessType_HTLC_OFFERED_REMOTE_TIMEOUT WitnessType = 8 + // + //A witness that allows us to sweep an HTLC that was offered to us by the + //remote party. We use this witness in the case that the remote party goes to + //chain, and we know the pre-image to the HTLC. We can sweep this without any + //additional timeout. + WitnessType_HTLC_ACCEPTED_REMOTE_SUCCESS WitnessType = 9 + // + //A witness that allows us to sweep an HTLC from the remote party's commitment + //transaction in the case that the broadcast a revoked commitment, but then + //also immediately attempt to go to the second level to claim the HTLC. + WitnessType_HTLC_SECOND_LEVEL_REVOKE WitnessType = 10 + // + //A witness type that allows us to spend a regular p2wkh output that's sent to + //an output which is under complete control of the backing wallet. + WitnessType_WITNESS_KEY_HASH WitnessType = 11 + // + //A witness type that allows us to sweep an output that sends to a nested P2SH + //script that pays to a key solely under our control. + WitnessType_NESTED_WITNESS_KEY_HASH WitnessType = 12 +) + +var WitnessType_name = map[int32]string{ + 0: "UNKNOWN_WITNESS", + 1: "COMMITMENT_TIME_LOCK", + 2: "COMMITMENT_NO_DELAY", + 3: "COMMITMENT_REVOKE", + 4: "HTLC_OFFERED_REVOKE", + 5: "HTLC_ACCEPTED_REVOKE", + 6: "HTLC_OFFERED_TIMEOUT_SECOND_LEVEL", + 7: "HTLC_ACCEPTED_SUCCESS_SECOND_LEVEL", + 8: "HTLC_OFFERED_REMOTE_TIMEOUT", + 9: "HTLC_ACCEPTED_REMOTE_SUCCESS", + 10: "HTLC_SECOND_LEVEL_REVOKE", + 11: "WITNESS_KEY_HASH", + 12: "NESTED_WITNESS_KEY_HASH", +} + +var WitnessType_value = map[string]int32{ + "UNKNOWN_WITNESS": 0, + "COMMITMENT_TIME_LOCK": 1, + "COMMITMENT_NO_DELAY": 2, + "COMMITMENT_REVOKE": 3, + "HTLC_OFFERED_REVOKE": 4, + "HTLC_ACCEPTED_REVOKE": 5, + "HTLC_OFFERED_TIMEOUT_SECOND_LEVEL": 6, + "HTLC_ACCEPTED_SUCCESS_SECOND_LEVEL": 7, + "HTLC_OFFERED_REMOTE_TIMEOUT": 8, + "HTLC_ACCEPTED_REMOTE_SUCCESS": 9, + "HTLC_SECOND_LEVEL_REVOKE": 10, + "WITNESS_KEY_HASH": 11, + "NESTED_WITNESS_KEY_HASH": 12, +} + +func (x WitnessType) String() string { + return proto.EnumName(WitnessType_name, int32(x)) +} + +func (WitnessType) EnumDescriptor() ([]byte, []int) { + return fileDescriptor_6cc6942ac78249e5, []int{0} +} + type KeyReq struct { //* //Is the key finger print of the root pubkey that this request is targeting. @@ -411,7 +514,170 @@ func (m *EstimateFeeResponse) GetSatPerKw() int64 { return 0 } +type PendingSweep struct { + // The outpoint of the output we're attempting to sweep. + Outpoint *lnrpc.OutPoint `protobuf:"bytes,1,opt,name=outpoint,proto3" json:"outpoint,omitempty"` + // The witness type of the output we're attempting to sweep. + WitnessType WitnessType `protobuf:"varint,2,opt,name=witness_type,proto3,enum=walletrpc.WitnessType" json:"witness_type,omitempty"` + // The value of the output we're attempting to sweep. + AmountSat uint32 `protobuf:"varint,3,opt,name=amount_sat,proto3" json:"amount_sat,omitempty"` + // + //The fee rate we'll use to sweep the output. The fee rate is only determined + //once a sweeping transaction for the output is created, so it's possible for + //this to be 0 before this. + SatPerByte uint32 `protobuf:"varint,4,opt,name=sat_per_byte,proto3" json:"sat_per_byte,omitempty"` + // The number of broadcast attempts we've made to sweep the output. + BroadcastAttempts uint32 `protobuf:"varint,5,opt,name=broadcast_attempts,proto3" json:"broadcast_attempts,omitempty"` + // + //The next height of the chain at which we'll attempt to broadcast the + //sweep transaction of the output. + NextBroadcastHeight uint32 `protobuf:"varint,6,opt,name=next_broadcast_height,proto3" json:"next_broadcast_height,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *PendingSweep) Reset() { *m = PendingSweep{} } +func (m *PendingSweep) String() string { return proto.CompactTextString(m) } +func (*PendingSweep) ProtoMessage() {} +func (*PendingSweep) Descriptor() ([]byte, []int) { + return fileDescriptor_6cc6942ac78249e5, []int{9} +} + +func (m *PendingSweep) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_PendingSweep.Unmarshal(m, b) +} +func (m *PendingSweep) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_PendingSweep.Marshal(b, m, deterministic) +} +func (m *PendingSweep) XXX_Merge(src proto.Message) { + xxx_messageInfo_PendingSweep.Merge(m, src) +} +func (m *PendingSweep) XXX_Size() int { + return xxx_messageInfo_PendingSweep.Size(m) +} +func (m *PendingSweep) XXX_DiscardUnknown() { + xxx_messageInfo_PendingSweep.DiscardUnknown(m) +} + +var xxx_messageInfo_PendingSweep proto.InternalMessageInfo + +func (m *PendingSweep) GetOutpoint() *lnrpc.OutPoint { + if m != nil { + return m.Outpoint + } + return nil +} + +func (m *PendingSweep) GetWitnessType() WitnessType { + if m != nil { + return m.WitnessType + } + return WitnessType_UNKNOWN_WITNESS +} + +func (m *PendingSweep) GetAmountSat() uint32 { + if m != nil { + return m.AmountSat + } + return 0 +} + +func (m *PendingSweep) GetSatPerByte() uint32 { + if m != nil { + return m.SatPerByte + } + return 0 +} + +func (m *PendingSweep) GetBroadcastAttempts() uint32 { + if m != nil { + return m.BroadcastAttempts + } + return 0 +} + +func (m *PendingSweep) GetNextBroadcastHeight() uint32 { + if m != nil { + return m.NextBroadcastHeight + } + return 0 +} + +type PendingSweepsRequest struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *PendingSweepsRequest) Reset() { *m = PendingSweepsRequest{} } +func (m *PendingSweepsRequest) String() string { return proto.CompactTextString(m) } +func (*PendingSweepsRequest) ProtoMessage() {} +func (*PendingSweepsRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_6cc6942ac78249e5, []int{10} +} + +func (m *PendingSweepsRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_PendingSweepsRequest.Unmarshal(m, b) +} +func (m *PendingSweepsRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_PendingSweepsRequest.Marshal(b, m, deterministic) +} +func (m *PendingSweepsRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_PendingSweepsRequest.Merge(m, src) +} +func (m *PendingSweepsRequest) XXX_Size() int { + return xxx_messageInfo_PendingSweepsRequest.Size(m) +} +func (m *PendingSweepsRequest) XXX_DiscardUnknown() { + xxx_messageInfo_PendingSweepsRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_PendingSweepsRequest proto.InternalMessageInfo + +type PendingSweepsResponse struct { + // + //The set of outputs currently being swept by lnd's central batching engine. + PendingSweeps []*PendingSweep `protobuf:"bytes,1,rep,name=pending_sweeps,proto3" json:"pending_sweeps,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *PendingSweepsResponse) Reset() { *m = PendingSweepsResponse{} } +func (m *PendingSweepsResponse) String() string { return proto.CompactTextString(m) } +func (*PendingSweepsResponse) ProtoMessage() {} +func (*PendingSweepsResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_6cc6942ac78249e5, []int{11} +} + +func (m *PendingSweepsResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_PendingSweepsResponse.Unmarshal(m, b) +} +func (m *PendingSweepsResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_PendingSweepsResponse.Marshal(b, m, deterministic) +} +func (m *PendingSweepsResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_PendingSweepsResponse.Merge(m, src) +} +func (m *PendingSweepsResponse) XXX_Size() int { + return xxx_messageInfo_PendingSweepsResponse.Size(m) +} +func (m *PendingSweepsResponse) XXX_DiscardUnknown() { + xxx_messageInfo_PendingSweepsResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_PendingSweepsResponse proto.InternalMessageInfo + +func (m *PendingSweepsResponse) GetPendingSweeps() []*PendingSweep { + if m != nil { + return m.PendingSweeps + } + return nil +} + func init() { + proto.RegisterEnum("walletrpc.WitnessType", WitnessType_name, WitnessType_value) proto.RegisterType((*KeyReq)(nil), "walletrpc.KeyReq") proto.RegisterType((*AddrRequest)(nil), "walletrpc.AddrRequest") proto.RegisterType((*AddrResponse)(nil), "walletrpc.AddrResponse") @@ -421,45 +687,73 @@ func init() { proto.RegisterType((*SendOutputsResponse)(nil), "walletrpc.SendOutputsResponse") proto.RegisterType((*EstimateFeeRequest)(nil), "walletrpc.EstimateFeeRequest") proto.RegisterType((*EstimateFeeResponse)(nil), "walletrpc.EstimateFeeResponse") + proto.RegisterType((*PendingSweep)(nil), "walletrpc.PendingSweep") + proto.RegisterType((*PendingSweepsRequest)(nil), "walletrpc.PendingSweepsRequest") + proto.RegisterType((*PendingSweepsResponse)(nil), "walletrpc.PendingSweepsResponse") } func init() { proto.RegisterFile("walletrpc/walletkit.proto", fileDescriptor_6cc6942ac78249e5) } var fileDescriptor_6cc6942ac78249e5 = []byte{ - // 524 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x53, 0x5d, 0x6f, 0xd3, 0x30, - 0x14, 0xd5, 0x56, 0x56, 0xd6, 0xdb, 0x76, 0x80, 0xcb, 0x46, 0x89, 0x18, 0x4c, 0x81, 0x87, 0x3e, - 0xa0, 0x54, 0x6c, 0x02, 0x21, 0x78, 0x02, 0x6d, 0xd3, 0xa4, 0x4e, 0xac, 0x84, 0x4a, 0x48, 0x08, - 0x29, 0x72, 0x93, 0xbb, 0xd4, 0x6a, 0x6a, 0x67, 0xce, 0x0d, 0x49, 0xfe, 0x0f, 0x3f, 0x14, 0xe5, - 0xa3, 0x5d, 0x4a, 0x19, 0x4f, 0x71, 0x8e, 0xcf, 0x3d, 0xbe, 0xd7, 0xe7, 0x18, 0x9e, 0x26, 0x3c, - 0x08, 0x90, 0x74, 0xe8, 0x0e, 0xcb, 0xd5, 0x5c, 0x90, 0x15, 0x6a, 0x45, 0x8a, 0xb5, 0x56, 0x5b, - 0xc6, 0xe3, 0x48, 0xf8, 0x32, 0xe7, 0xe4, 0x5f, 0xd4, 0x25, 0xc1, 0xfc, 0x0a, 0xcd, 0x11, 0x66, - 0x36, 0xde, 0xb0, 0x01, 0x3c, 0x9c, 0x63, 0xe6, 0x5c, 0x0b, 0xe9, 0xa3, 0x76, 0x42, 0x2d, 0x24, - 0xf5, 0xb7, 0x8e, 0xb6, 0x06, 0x3b, 0xf6, 0xde, 0x1c, 0xb3, 0xf3, 0x02, 0x1e, 0xe7, 0x28, 0x3b, - 0x04, 0x28, 0x98, 0x7c, 0x21, 0x82, 0xac, 0xbf, 0x5d, 0x70, 0x5a, 0x39, 0xa7, 0x00, 0xcc, 0x2e, - 0xb4, 0x3f, 0x79, 0x9e, 0xb6, 0xf1, 0x26, 0xc6, 0x88, 0x4c, 0x13, 0x3a, 0xe5, 0x6f, 0x14, 0x2a, - 0x19, 0x21, 0x63, 0x70, 0x8f, 0x7b, 0x9e, 0x2e, 0xb4, 0x5b, 0x76, 0xb1, 0x36, 0x5f, 0x41, 0x7b, - 0xa2, 0xb9, 0x8c, 0xb8, 0x4b, 0x42, 0x49, 0xb6, 0x0f, 0x4d, 0x4a, 0x9d, 0x19, 0xa6, 0x05, 0xa9, - 0x63, 0xef, 0x50, 0x7a, 0x81, 0xa9, 0xf9, 0x0e, 0x1e, 0x8c, 0xe3, 0x69, 0x20, 0xa2, 0xd9, 0x4a, - 0xec, 0x25, 0x74, 0xc3, 0x12, 0x72, 0x50, 0x6b, 0xb5, 0x54, 0xed, 0x54, 0xe0, 0x59, 0x8e, 0x99, - 0x3f, 0x81, 0x7d, 0x43, 0xe9, 0x5d, 0xc5, 0x14, 0xc6, 0x14, 0x55, 0x7d, 0xb1, 0x67, 0x00, 0x11, - 0x27, 0x27, 0x44, 0xed, 0xcc, 0x93, 0xa2, 0xae, 0x61, 0xef, 0x46, 0x9c, 0xc6, 0xa8, 0x47, 0x09, - 0x1b, 0xc0, 0x7d, 0x55, 0xf2, 0xfb, 0xdb, 0x47, 0x8d, 0x41, 0xfb, 0x78, 0xcf, 0xaa, 0xee, 0xcf, - 0x9a, 0xa4, 0x57, 0x31, 0xd9, 0xcb, 0x6d, 0xf3, 0x35, 0xf4, 0xd6, 0xd4, 0xab, 0xce, 0xf6, 0xa1, - 0xa9, 0x79, 0xe2, 0xd0, 0x6a, 0x06, 0xcd, 0x93, 0x49, 0x6a, 0xbe, 0x05, 0x76, 0x16, 0x91, 0x58, - 0x70, 0xc2, 0x73, 0xc4, 0x65, 0x2f, 0x2f, 0xa0, 0xed, 0x2a, 0x79, 0xed, 0x10, 0xd7, 0x3e, 0x2e, - 0xaf, 0x1d, 0x72, 0x68, 0x52, 0x20, 0xe6, 0x09, 0xf4, 0xd6, 0xca, 0xaa, 0x43, 0xfe, 0x3b, 0xc3, - 0xf1, 0xef, 0x06, 0xb4, 0xbe, 0x17, 0xfe, 0x8f, 0x04, 0xb1, 0x0f, 0xd0, 0x3d, 0x45, 0x2d, 0x7e, - 0xe1, 0x17, 0x4c, 0x69, 0x84, 0x19, 0x7b, 0x64, 0xad, 0xc2, 0x61, 0x95, 0x19, 0x30, 0x0e, 0x56, - 0x43, 0x8e, 0x30, 0x3b, 0xc5, 0xc8, 0xd5, 0x22, 0x24, 0xa5, 0xd9, 0x7b, 0x68, 0x95, 0xb5, 0x79, - 0x5d, 0xaf, 0x4e, 0xba, 0x54, 0x2e, 0x27, 0xa5, 0xef, 0xac, 0xfc, 0x08, 0xbb, 0xf9, 0x79, 0x79, - 0x02, 0xd8, 0x41, 0xed, 0xc0, 0x5a, 0x42, 0x8c, 0x27, 0x1b, 0x78, 0x35, 0xde, 0x05, 0xb0, 0xca, - 0xf0, 0x7a, 0x3a, 0xea, 0x32, 0x35, 0xdc, 0x30, 0x6a, 0xf8, 0xdf, 0x39, 0xb9, 0x84, 0x76, 0xcd, - 0x24, 0x76, 0x58, 0xa3, 0x6e, 0x46, 0xc3, 0x78, 0x7e, 0xd7, 0xf6, 0xad, 0x5a, 0xcd, 0x8d, 0x35, - 0xb5, 0x4d, 0x73, 0xd7, 0xd4, 0xfe, 0x61, 0xe2, 0xe7, 0x37, 0x3f, 0x86, 0xbe, 0xa0, 0x59, 0x3c, - 0xb5, 0x5c, 0xb5, 0x18, 0x06, 0xc2, 0x9f, 0x91, 0x14, 0xd2, 0x97, 0x48, 0x89, 0xd2, 0xf3, 0x61, - 0x20, 0xbd, 0x61, 0x20, 0x6f, 0x1f, 0xb7, 0x0e, 0xdd, 0x69, 0xb3, 0x78, 0xbc, 0x27, 0x7f, 0x02, - 0x00, 0x00, 0xff, 0xff, 0x2d, 0xbb, 0xcd, 0x97, 0xfa, 0x03, 0x00, 0x00, + // 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, } // Reference imports to suppress errors if they are not otherwise used. @@ -502,6 +796,16 @@ type WalletKitClient interface { //determine the fee (in sat/kw) to attach to a transaction in order to //achieve the confirmation target. EstimateFee(ctx context.Context, in *EstimateFeeRequest, opts ...grpc.CallOption) (*EstimateFeeResponse, error) + // + //PendingSweeps returns lists of on-chain outputs that lnd is currently + //attempting to sweep within its central batching engine. Outputs with similar + //fee rates are batched together in order to sweep them within a single + //transaction. + // + //NOTE: Some of the fields within PendingSweepsRequest are not guaranteed to + //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) } type walletKitClient struct { @@ -566,6 +870,15 @@ func (c *walletKitClient) EstimateFee(ctx context.Context, in *EstimateFeeReques return out, nil } +func (c *walletKitClient) PendingSweeps(ctx context.Context, in *PendingSweepsRequest, opts ...grpc.CallOption) (*PendingSweepsResponse, error) { + out := new(PendingSweepsResponse) + err := c.cc.Invoke(ctx, "/walletrpc.WalletKit/PendingSweeps", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // WalletKitServer is the server API for WalletKit service. type WalletKitServer interface { //* @@ -596,6 +909,16 @@ type WalletKitServer interface { //determine the fee (in sat/kw) to attach to a transaction in order to //achieve the confirmation target. EstimateFee(context.Context, *EstimateFeeRequest) (*EstimateFeeResponse, error) + // + //PendingSweeps returns lists of on-chain outputs that lnd is currently + //attempting to sweep within its central batching engine. Outputs with similar + //fee rates are batched together in order to sweep them within a single + //transaction. + // + //NOTE: Some of the fields within PendingSweepsRequest are not guaranteed to + //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) } func RegisterWalletKitServer(s *grpc.Server, srv WalletKitServer) { @@ -710,6 +1033,24 @@ func _WalletKit_EstimateFee_Handler(srv interface{}, ctx context.Context, dec fu return interceptor(ctx, in, info, handler) } +func _WalletKit_PendingSweeps_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PendingSweepsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WalletKitServer).PendingSweeps(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/walletrpc.WalletKit/PendingSweeps", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WalletKitServer).PendingSweeps(ctx, req.(*PendingSweepsRequest)) + } + return interceptor(ctx, in, info, handler) +} + var _WalletKit_serviceDesc = grpc.ServiceDesc{ ServiceName: "walletrpc.WalletKit", HandlerType: (*WalletKitServer)(nil), @@ -738,6 +1079,10 @@ var _WalletKit_serviceDesc = grpc.ServiceDesc{ MethodName: "EstimateFee", Handler: _WalletKit_EstimateFee_Handler, }, + { + MethodName: "PendingSweeps", + Handler: _WalletKit_PendingSweeps_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "walletrpc/walletkit.proto", diff --git a/lnrpc/walletrpc/walletkit.proto b/lnrpc/walletrpc/walletkit.proto index e1b0518bd..eba003442 100644 --- a/lnrpc/walletrpc/walletkit.proto +++ b/lnrpc/walletrpc/walletkit.proto @@ -1,5 +1,6 @@ syntax = "proto3"; +import "rpc.proto"; import "signrpc/signer.proto"; package walletrpc; @@ -81,6 +82,127 @@ message EstimateFeeResponse { int64 sat_per_kw = 1; } +enum WitnessType { + UNKNOWN_WITNESS = 0; + + /* + A witness that allows us to spend the output of a commitment transaction + after a relative lock-time lockout. + */ + COMMITMENT_TIME_LOCK = 1; + + /* + A witness that allows us to spend a settled no-delay output immediately on a + counterparty's commitment transaction. + */ + COMMITMENT_NO_DELAY = 2; + + /* + A witness that allows us to sweep the settled output of a malicious + counterparty's who broadcasts a revoked commitment transaction. + */ + COMMITMENT_REVOKE = 3; + + /* + A witness that allows us to sweep an HTLC which we offered to the remote + party in the case that they broadcast a revoked commitment state. + */ + HTLC_OFFERED_REVOKE = 4; + + /* + A witness that allows us to sweep an HTLC output sent to us in the case that + the remote party broadcasts a revoked commitment state. + */ + HTLC_ACCEPTED_REVOKE = 5; + + /* + A witness that allows us to sweep an HTLC output that we extended to a + party, but was never fulfilled. This HTLC output isn't directly on the + commitment transaction, but is the result of a confirmed second-level HTLC + transaction. As a result, we can only spend this after a CSV delay. + */ + HTLC_OFFERED_TIMEOUT_SECOND_LEVEL = 6; + + /* + A witness that allows us to sweep an HTLC output that was offered to us, and + for which we have a payment preimage. This HTLC output isn't directly on our + commitment transaction, but is the result of confirmed second-level HTLC + transaction. As a result, we can only spend this after a CSV delay. + */ + HTLC_ACCEPTED_SUCCESS_SECOND_LEVEL = 7; + + /* + A witness that allows us to sweep an HTLC that we offered to the remote + party which lies in the commitment transaction of the remote party. We can + spend this output after the absolute CLTV timeout of the HTLC as passed. + */ + HTLC_OFFERED_REMOTE_TIMEOUT = 8; + + /* + A witness that allows us to sweep an HTLC that was offered to us by the + remote party. We use this witness in the case that the remote party goes to + chain, and we know the pre-image to the HTLC. We can sweep this without any + additional timeout. + */ + HTLC_ACCEPTED_REMOTE_SUCCESS = 9; + + /* + A witness that allows us to sweep an HTLC from the remote party's commitment + transaction in the case that the broadcast a revoked commitment, but then + also immediately attempt to go to the second level to claim the HTLC. + */ + HTLC_SECOND_LEVEL_REVOKE = 10; + + /* + A witness type that allows us to spend a regular p2wkh output that's sent to + an output which is under complete control of the backing wallet. + */ + WITNESS_KEY_HASH = 11; + + /* + A witness type that allows us to sweep an output that sends to a nested P2SH + script that pays to a key solely under our control. + */ + NESTED_WITNESS_KEY_HASH = 12; +} + +message PendingSweep { + // The outpoint of the output we're attempting to sweep. + lnrpc.OutPoint outpoint = 1 [json_name = "outpoint"]; + + // The witness type of the output we're attempting to sweep. + WitnessType witness_type = 2 [json_name = "witness_type"]; + + // The value of the output we're attempting to sweep. + uint32 amount_sat = 3 [json_name = "amount_sat"]; + + /* + The fee rate we'll use to sweep the output. The fee rate is only determined + once a sweeping transaction for the output is created, so it's possible for + this to be 0 before this. + */ + uint32 sat_per_byte = 4 [json_name = "sat_per_byte"]; + + // The number of broadcast attempts we've made to sweep the output. + uint32 broadcast_attempts = 5 [json_name = "broadcast_attempts"]; + + /* + The next height of the chain at which we'll attempt to broadcast the + sweep transaction of the output. + */ + uint32 next_broadcast_height = 6 [json_name = "next_broadcast_height"]; +} + +message PendingSweepsRequest { +} + +message PendingSweepsResponse { + /* + The set of outputs currently being swept by lnd's central batching engine. + */ + repeated PendingSweep pending_sweeps = 1 [json_name = "pending_sweeps"]; +} + service WalletKit { /** DeriveNextKey attempts to derive the *next* key within the key family @@ -121,4 +243,16 @@ service WalletKit { achieve the confirmation target. */ rpc EstimateFee(EstimateFeeRequest) returns (EstimateFeeResponse); + + /* + PendingSweeps returns lists of on-chain outputs that lnd is currently + attempting to sweep within its central batching engine. Outputs with similar + fee rates are batched together in order to sweep them within a single + transaction. + + NOTE: Some of the fields within PendingSweepsRequest are not guaranteed to + remain supported. This is an advanced API that depends on the internals of + the UtxoSweeper, so things may change. + */ + rpc PendingSweeps(PendingSweepsRequest) returns (PendingSweepsResponse); } diff --git a/lnrpc/walletrpc/walletkit_server.go b/lnrpc/walletrpc/walletkit_server.go index 3e998fe23..f2e04f593 100644 --- a/lnrpc/walletrpc/walletkit_server.go +++ b/lnrpc/walletrpc/walletkit_server.go @@ -10,6 +10,7 @@ import ( "path/filepath" "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" @@ -74,6 +75,10 @@ var ( Entity: "onchain", Action: "read", }}, + "/walletrpc.WalletKit/PendingSweeps": {{ + Entity: "onchain", + Action: "read", + }}, } // DefaultWalletKitMacFilename is the default name of the wallet kit @@ -331,3 +336,76 @@ func (w *WalletKit) EstimateFee(ctx context.Context, SatPerKw: int64(satPerKw), }, nil } + +// PendingSweeps returns lists of on-chain outputs that lnd is currently +// attempting to sweep within its central batching engine. Outputs with similar +// fee rates are batched together in order to sweep them within a single +// transaction. The fee rate of each sweeping transaction is determined by +// taking the average fee rate of all the outputs it's trying to sweep. +func (w *WalletKit) PendingSweeps(ctx context.Context, + in *PendingSweepsRequest) (*PendingSweepsResponse, error) { + + // Retrieve all of the outputs the UtxoSweeper is currently trying to + // sweep. + pendingInputs, err := w.cfg.Sweeper.PendingInputs() + if err != nil { + return nil, err + } + + // Convert them into their respective RPC format. + rpcPendingSweeps := make([]*PendingSweep, 0, len(pendingInputs)) + for _, pendingInput := range pendingInputs { + var witnessType WitnessType + switch pendingInput.WitnessType { + case input.CommitmentTimeLock: + witnessType = WitnessType_COMMITMENT_TIME_LOCK + case input.CommitmentNoDelay: + witnessType = WitnessType_COMMITMENT_NO_DELAY + case input.CommitmentRevoke: + witnessType = WitnessType_COMMITMENT_REVOKE + case input.HtlcOfferedRevoke: + witnessType = WitnessType_HTLC_OFFERED_REVOKE + case input.HtlcAcceptedRevoke: + witnessType = WitnessType_HTLC_ACCEPTED_REVOKE + case input.HtlcOfferedTimeoutSecondLevel: + witnessType = WitnessType_HTLC_OFFERED_TIMEOUT_SECOND_LEVEL + case input.HtlcAcceptedSuccessSecondLevel: + witnessType = WitnessType_HTLC_ACCEPTED_SUCCESS_SECOND_LEVEL + case input.HtlcOfferedRemoteTimeout: + witnessType = WitnessType_HTLC_OFFERED_REMOTE_TIMEOUT + case input.HtlcAcceptedRemoteSuccess: + witnessType = WitnessType_HTLC_ACCEPTED_REMOTE_SUCCESS + case input.HtlcSecondLevelRevoke: + witnessType = WitnessType_HTLC_SECOND_LEVEL_REVOKE + case input.WitnessKeyHash: + witnessType = WitnessType_WITNESS_KEY_HASH + case input.NestedWitnessKeyHash: + witnessType = WitnessType_NESTED_WITNESS_KEY_HASH + default: + log.Warnf("Unhandled witness type %v for input %v", + pendingInput.WitnessType, pendingInput.OutPoint) + } + + op := &lnrpc.OutPoint{ + TxidBytes: pendingInput.OutPoint.Hash[:], + OutputIndex: pendingInput.OutPoint.Index, + } + amountSat := uint32(pendingInput.Amount) + satPerByte := uint32(pendingInput.LastFeeRate.FeePerKVByte() / 1000) + broadcastAttempts := uint32(pendingInput.BroadcastAttempts) + nextBroadcastHeight := uint32(pendingInput.NextBroadcastHeight) + + rpcPendingSweeps = append(rpcPendingSweeps, &PendingSweep{ + Outpoint: op, + WitnessType: witnessType, + AmountSat: amountSat, + SatPerByte: satPerByte, + BroadcastAttempts: broadcastAttempts, + NextBroadcastHeight: nextBroadcastHeight, + }) + } + + return &PendingSweepsResponse{ + PendingSweeps: rpcPendingSweeps, + }, nil +} diff --git a/rpcserver.go b/rpcserver.go index b827b65b1..dca0a93a9 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -491,7 +491,7 @@ func newRPCServer(s *server, macService *macaroons.Service, err = subServerCgs.PopulateDependencies( s.cc, networkDir, macService, atpl, invoiceRegistry, s.htlcSwitch, activeNetParams.Params, s.chanRouter, - routerBackend, s.nodeSigner, s.chanDB, + routerBackend, s.nodeSigner, s.chanDB, s.sweeper, ) if err != nil { return nil, err diff --git a/subrpcserver_config.go b/subrpcserver_config.go index d99faeab6..cb9b20768 100644 --- a/subrpcserver_config.go +++ b/subrpcserver_config.go @@ -18,6 +18,7 @@ import ( "github.com/lightningnetwork/lnd/macaroons" "github.com/lightningnetwork/lnd/netann" "github.com/lightningnetwork/lnd/routing" + "github.com/lightningnetwork/lnd/sweep" ) // subRPCServerConfigs is special sub-config in the main configuration that @@ -72,7 +73,8 @@ func (s *subRPCServerConfigs) PopulateDependencies(cc *chainControl, chanRouter *routing.ChannelRouter, routerBackend *routerrpc.RouterBackend, nodeSigner *netann.NodeSigner, - chanDB *channeldb.DB) error { + chanDB *channeldb.DB, + sweeper *sweep.UtxoSweeper) error { // First, we'll use reflect to obtain a version of the config struct // that allows us to programmatically inspect its fields. @@ -129,6 +131,9 @@ func (s *subRPCServerConfigs) PopulateDependencies(cc *chainControl, subCfgValue.FieldByName("KeyRing").Set( reflect.ValueOf(cc.keyRing), ) + subCfgValue.FieldByName("Sweeper").Set( + reflect.ValueOf(sweeper), + ) case *autopilotrpc.Config: subCfgValue := extractReflectValue(subCfg) diff --git a/sweep/sweeper.go b/sweep/sweeper.go index 9c8e59c7b..6fe398559 100644 --- a/sweep/sweeper.go +++ b/sweep/sweeper.go @@ -46,6 +46,11 @@ var ( // for the configured max number of attempts. ErrTooManyAttempts = errors.New("sweep failed after max attempts") + // 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. + ErrSweeperShuttingDown = errors.New("utxo sweeper shutting down") + // DefaultMaxSweepAttempts specifies the default maximum number of times // an input is included in a publish attempt before giving up and // returning an error to the caller. @@ -80,6 +85,10 @@ type pendingInput struct { // map it into a fee rate whenever we attempt to cluster inputs for a // sweep. feePreference FeePreference + + // lastFeeRate is the most recent fee rate used for this input within a + // transaction broadcast to the network. + lastFeeRate lnwallet.SatPerKWeight } // pendingInputs is a type alias for a set of pending inputs. @@ -92,6 +101,38 @@ type inputCluster struct { inputs pendingInputs } +// pendingSweepsReq is an internal message we'll use to represent an external +// caller's intent to retrieve all of the pending inputs the UtxoSweeper is +// attempting to sweep. +type pendingSweepsReq struct { + respChan chan map[wire.OutPoint]*PendingInput +} + +// PendingInput contains information about an input that is currently being +// swept by the UtxoSweeper. +type PendingInput struct { + // OutPoint is the identify outpoint of the input being swept. + OutPoint wire.OutPoint + + // WitnessType is the witness type of the input being swept. + WitnessType input.WitnessType + + // Amount is the amount of the input being swept. + Amount btcutil.Amount + + // LastFeeRate is the most recent fee rate used for the input being + // swept within a transaction broadcast to the network. + LastFeeRate lnwallet.SatPerKWeight + + // BroadcastAttempts is the number of attempts we've made to sweept the + // input. + BroadcastAttempts int + + // NextBroadcastHeight is the next height of the chain at which we'll + // attempt to broadcast a transaction sweeping the input. + NextBroadcastHeight uint32 +} + // UtxoSweeper is responsible for sweeping outputs back into the wallet type UtxoSweeper struct { started uint32 // To be used atomically. @@ -102,6 +143,11 @@ type UtxoSweeper struct { newInputs chan *sweepInputMessage spendChan chan *chainntnfs.SpendDetail + // pendingSweepsReq is a channel that will be sent requests by external + // callers in order to retrieve the set of pending inputs the + // UtxoSweeper is attempting to sweep. + pendingSweepsReqs chan *pendingSweepsReq + // pendingInputs is the total set of inputs the UtxoSweeper has been // requested to sweep. pendingInputs pendingInputs @@ -208,11 +254,12 @@ type sweepInputMessage struct { // New returns a new Sweeper instance. func New(cfg *UtxoSweeperConfig) *UtxoSweeper { return &UtxoSweeper{ - cfg: cfg, - newInputs: make(chan *sweepInputMessage), - spendChan: make(chan *chainntnfs.SpendDetail), - quit: make(chan struct{}), - pendingInputs: make(pendingInputs), + cfg: cfg, + newInputs: make(chan *sweepInputMessage), + spendChan: make(chan *chainntnfs.SpendDetail), + pendingSweepsReqs: make(chan *pendingSweepsReq), + quit: make(chan struct{}), + pendingInputs: make(pendingInputs), } } @@ -336,7 +383,7 @@ func (s *UtxoSweeper) SweepInput(input input.Input, select { case s.newInputs <- sweeperInput: case <-s.quit: - return nil, fmt.Errorf("sweeper shutting down") + return nil, ErrSweeperShuttingDown } return sweeperInput.resultChan, nil @@ -480,6 +527,11 @@ func (s *UtxoSweeper) collector(blockEpochs <-chan *chainntnfs.BlockEpoch, log.Errorf("schedule sweep: %v", err) } + // A new external request has been received to retrieve all of + // the inputs we're currently attempting to sweep. + case req := <-s.pendingSweepsReqs: + req.respChan <- s.handlePendingSweepsReq(req) + // The timer expires and we are going to (re)sweep. case <-s.timer: log.Debugf("Sweep timer expired") @@ -580,6 +632,8 @@ func (s *UtxoSweeper) clusterBySweepFeeRate() []inputCluster { inputs = make(pendingInputs) bucketInputs[bucket] = inputs } + + input.lastFeeRate = feeRate inputs[op] = input inputFeeRates[op] = feeRate } @@ -880,6 +934,51 @@ func (s *UtxoSweeper) waitForSpend(outpoint wire.OutPoint, return spendEvent.Cancel, nil } +// PendingInputs returns the set of inputs that the UtxoSweeper is currently +// attempting to sweep. +func (s *UtxoSweeper) PendingInputs() (map[wire.OutPoint]*PendingInput, error) { + respChan := make(chan map[wire.OutPoint]*PendingInput, 1) + select { + case s.pendingSweepsReqs <- &pendingSweepsReq{ + respChan: respChan, + }: + case <-s.quit: + return nil, ErrSweeperShuttingDown + } + + select { + case pendingSweeps := <-respChan: + return pendingSweeps, nil + case <-s.quit: + return nil, ErrSweeperShuttingDown + } +} + +// handlePendingSweepsReq handles a request to retrieve all pending inputs the +// UtxoSweeper is attempting to sweep. +func (s *UtxoSweeper) handlePendingSweepsReq( + req *pendingSweepsReq) map[wire.OutPoint]*PendingInput { + + pendingInputs := make(map[wire.OutPoint]*PendingInput, len(s.pendingInputs)) + for _, pendingInput := range s.pendingInputs { + // Only the exported fields are set, as we expect the response + // to only be consumed externally. + op := *pendingInput.input.OutPoint() + pendingInputs[op] = &PendingInput{ + OutPoint: op, + WitnessType: pendingInput.input.WitnessType(), + Amount: btcutil.Amount( + pendingInput.input.SignDesc().Output.Value, + ), + LastFeeRate: pendingInput.lastFeeRate, + BroadcastAttempts: pendingInput.publishAttempts, + NextBroadcastHeight: uint32(pendingInput.minPublishHeight), + } + } + + return pendingInputs +} + // 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 f7ec2173c..d494b996b 100644 --- a/sweep/sweeper_test.go +++ b/sweep/sweeper_test.go @@ -263,6 +263,29 @@ func (ctx *sweeperTestContext) expectResult(c chan Result, expected error) { } } +func (ctx *sweeperTestContext) assertPendingInputs(inputs ...input.Input) { + ctx.t.Helper() + + inputSet := make(map[wire.OutPoint]struct{}, len(inputs)) + for _, input := range inputs { + inputSet[*input.OutPoint()] = struct{}{} + } + + pendingInputs, err := ctx.sweeper.PendingInputs() + if err != nil { + ctx.t.Fatal(err) + } + if len(pendingInputs) != len(inputSet) { + ctx.t.Fatalf("expected %d pending inputs, got %d", + len(inputSet), len(pendingInputs)) + } + for input := range pendingInputs { + if _, ok := inputSet[input]; !ok { + ctx.t.Fatalf("found unexpected input %v", input) + } + } +} + // receiveSpendTx receives the transaction sent through the given resultChan. func receiveSpendTx(t *testing.T, resultChan chan Result) *wire.MsgTx { t.Helper() @@ -1032,3 +1055,71 @@ func TestDifferentFeePreferences(t *testing.T) { ctx.finish(1) } + +// TestPendingInputs ensures that the sweeper correctly determines the inputs +// pending to be swept. +func TestPendingInputs(t *testing.T) { + ctx := createSweeperTestContext(t) + + // Throughout this test, we'll be attempting to sweep three inputs, two + // with the higher fee preference, and the last with the lower. We do + // this to ensure the sweeper can return all pending inputs, even those + // with different fee preferences. + const ( + lowFeeRate = 5000 + highFeeRate = 10000 + ) + + lowFeePref := FeePreference{ + ConfTarget: 12, + } + ctx.estimator.blocksToFee[lowFeePref.ConfTarget] = lowFeeRate + + highFeePref := FeePreference{ + ConfTarget: 6, + } + ctx.estimator.blocksToFee[highFeePref.ConfTarget] = highFeeRate + + input1 := spendableInputs[0] + resultChan1, err := ctx.sweeper.SweepInput(input1, highFeePref) + if err != nil { + t.Fatal(err) + } + input2 := spendableInputs[1] + if _, err := ctx.sweeper.SweepInput(input2, highFeePref); err != nil { + t.Fatal(err) + } + input3 := spendableInputs[2] + resultChan3, err := ctx.sweeper.SweepInput(input3, lowFeePref) + if err != nil { + t.Fatal(err) + } + + // We should expect to see all inputs pending. + ctx.assertPendingInputs(input1, input2, input3) + + // We should expect to see both sweep transactions broadcast. The higher + // fee rate sweep should be broadcast first. We'll remove the lower fee + // rate sweep to ensure we can detect pending inputs after a sweep. + // Once the higher fee rate sweep confirms, we should no longer see + // those inputs pending. + ctx.tick() + ctx.receiveTx() + lowFeeRateTx := ctx.receiveTx() + ctx.backend.deleteUnconfirmed(lowFeeRateTx.TxHash()) + ctx.backend.mine() + ctx.expectResult(resultChan1, nil) + ctx.assertPendingInputs(input3) + + // We'll then trigger a new block to rebroadcast the lower fee rate + // sweep. Once again we'll ensure those inputs are no longer pending + // once the sweep transaction confirms. + ctx.backend.notifier.NotifyEpoch(101) + ctx.tick() + ctx.receiveTx() + ctx.backend.mine() + ctx.expectResult(resultChan3, nil) + ctx.assertPendingInputs() + + ctx.finish(1) +}