diff --git a/.gitignore b/.gitignore index 11c67fe65c4..fecc9ff0db7 100644 --- a/.gitignore +++ b/.gitignore @@ -59,6 +59,9 @@ mobile/*_generated.go *.hex *.db *.bin +*.sqlite +*.sqlite-shm +*.sqlite-wal vendor *.idea diff --git a/cmd/commands/cmd_payments.go b/cmd/commands/cmd_payments.go index d13b52da2cd..cc3ec6ab7d4 100644 --- a/cmd/commands/cmd_payments.go +++ b/cmd/commands/cmd_payments.go @@ -1525,6 +1525,71 @@ func listPayments(ctx *cli.Context) error { return nil } +var listPaymentDuplicatesCommand = cli.Command{ + Name: "listpaymentduplicates", + Category: "Payments", + Usage: "List duplicate payments for a given payment hash.", + Hidden: true, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "payment_hash", + Usage: "hex-encoded payment hash to query", + }, + }, + Action: actionDecorator(listPaymentDuplicates), +} + +func listPaymentDuplicates(ctx *cli.Context) error { + if !ctx.IsSet("payment_hash") { + return fmt.Errorf("payment_hash is required") + } + + hashBytes, err := hex.DecodeString(ctx.String("payment_hash")) + if err != nil { + return fmt.Errorf("error decoding payment_hash: %w", err) + } + + ctxc := getContext() + client, cleanUp := getClient(ctx) + defer cleanUp() + + req := &lnrpc.ListPaymentDuplicatesRequest{ + PaymentHash: hashBytes, + } + + resp, err := client.ListPaymentDuplicates(ctxc, req) + if err != nil { + return err + } + + printRespJSON(resp) + return nil +} + +var listAllPaymentDuplicatesCommand = cli.Command{ + Name: "listallpaymentduplicates", + Category: "Payments", + Usage: "List duplicate payments across all payments.", + Hidden: true, + Action: actionDecorator(listAllPaymentDuplicates), +} + +func listAllPaymentDuplicates(ctx *cli.Context) error { + ctxc := getContext() + client, cleanUp := getClient(ctx) + defer cleanUp() + + req := &lnrpc.ListAllPaymentDuplicatesRequest{} + + resp, err := client.ListAllPaymentDuplicates(ctxc, req) + if err != nil { + return err + } + + printRespJSON(resp) + return nil +} + var forwardingHistoryCommand = cli.Command{ Name: "fwdinghistory", Category: "Payments", diff --git a/cmd/commands/main.go b/cmd/commands/main.go index a11b63b9d33..be514cc938e 100644 --- a/cmd/commands/main.go +++ b/cmd/commands/main.go @@ -482,6 +482,8 @@ func Main() { ListChannelsCommand, closedChannelsCommand, listPaymentsCommand, + listPaymentDuplicatesCommand, + listAllPaymentDuplicatesCommand, describeGraphCommand, getNodeMetricsCommand, getChanInfoCommand, diff --git a/config_builder.go b/config_builder.go index 7ce63041ee2..23ef6662d13 100644 --- a/config_builder.go +++ b/config_builder.go @@ -51,6 +51,8 @@ import ( "github.com/lightningnetwork/lnd/macaroons" "github.com/lightningnetwork/lnd/msgmux" paymentsdb "github.com/lightningnetwork/lnd/payments/db" + paymentsmig1 "github.com/lightningnetwork/lnd/payments/db/migration1" + paymentsmig1sqlc "github.com/lightningnetwork/lnd/payments/db/migration1/sqlc" "github.com/lightningnetwork/lnd/rpcperms" "github.com/lightningnetwork/lnd/signal" "github.com/lightningnetwork/lnd/sqldb" @@ -76,6 +78,10 @@ const ( // graphMigration is the version number for the graph migration // that migrates the KV graph to the native SQL schema. graphMigration = 10 + + // paymentMigration is the version number for the payments migration + // that migrates KV payments to the native SQL schema. + paymentMigration = 12 ) // GrpcRegistrar is an interface that must be satisfied by an external subserver @@ -1153,6 +1159,32 @@ func (d *DefaultDatabaseBuilder) BuildDatabase( return nil } + paymentMig := func(tx *sqlc.Queries) error { + var err error + err = paymentsmig1.MigratePaymentsKVToSQL( + ctx, + dbs.ChanStateDB.Backend, + paymentsmig1sqlc.New(tx.GetTx()), + &paymentsmig1.SQLStoreConfig{ + QueryCfg: queryCfg, + }, + ) + if err != nil { + return fmt.Errorf("failed to migrate "+ + "payments to SQL: %w", err) + } + + // Set the payments bucket tombstone to + // indicate that the migration has been + // completed. + d.logger.Debugf("Setting payments bucket " + + "tombstone") + + return paymentsdb.SetPaymentsBucketTombstone( + dbs.ChanStateDB.Backend, + ) + } + // Make sure we attach the custom migration function to // the correct migration version. for i := 0; i < len(migrations); i++ { @@ -1162,11 +1194,17 @@ func (d *DefaultDatabaseBuilder) BuildDatabase( migrations[i].MigrationFn = invoiceMig continue + case graphMigration: migrations[i].MigrationFn = graphMig continue + case paymentMigration: + migrations[i].MigrationFn = paymentMig + + continue + default: } @@ -1228,6 +1266,23 @@ func (d *DefaultDatabaseBuilder) BuildDatabase( return nil, nil, err } + + // Create the payments DB. + // + // NOTE: In the regular build, this will construct a kvdb + // backed payments backend. With the test_native_sql tag, it + // will build a SQL payments backend. + sqlPaymentsDB, err := d.getPaymentsStore( + baseDB, dbs.ChanStateDB.Backend, + ) + if err != nil { + err = fmt.Errorf("unable to get payments store: %w", + err) + + return nil, nil, err + } + + dbs.PaymentsDB = sqlPaymentsDB } else { // Check if the invoice bucket tombstone is set. If it is, we // need to return and ask the user switch back to using the @@ -1248,6 +1303,27 @@ func (d *DefaultDatabaseBuilder) BuildDatabase( return nil, nil, err } + // Check if the payments bucket tombstone is set. If it is, we + // need to return and ask the user switch back to using the + // native SQL store. + ripPayments, err := paymentsdb.GetPaymentsBucketTombstone( + dbs.ChanStateDB.Backend, + ) + if err != nil { + err = fmt.Errorf("unable to check payments bucket "+ + "tombstone: %w", err) + d.logger.Error(err) + + return nil, nil, err + } + if ripPayments { + err = fmt.Errorf("payments bucket tombstoned, please " + + "switch back to native SQL") + d.logger.Error(err) + + return nil, nil, err + } + dbs.InvoiceDB = dbs.ChanStateDB graphStore, err = graphdb.NewKVStore( @@ -1256,40 +1332,32 @@ func (d *DefaultDatabaseBuilder) BuildDatabase( if err != nil { return nil, nil, err } - } - dbs.GraphDB, err = graphdb.NewChannelGraph(graphStore, chanGraphOpts...) - if err != nil { - cleanUp() + // Create the payments DB. + kvPaymentsDB, err := paymentsdb.NewKVStore( + dbs.ChanStateDB, + ) + if err != nil { + cleanUp() - err = fmt.Errorf("unable to open channel graph DB: %w", err) - d.logger.Error(err) + err = fmt.Errorf("unable to open payments DB: %w", err) + d.logger.Error(err) - return nil, nil, err - } + return nil, nil, err + } - // Mount the payments DB which is only KV for now. - // - // TODO(ziggie): Add support for SQL payments DB. - // Mount the payments DB for the KV store. - paymentsDBOptions := []paymentsdb.OptionModifier{ - paymentsdb.WithKeepFailedPaymentAttempts( - cfg.KeepFailedPaymentAttempts, - ), + dbs.PaymentsDB = kvPaymentsDB } - kvPaymentsDB, err := paymentsdb.NewKVStore( - dbs.ChanStateDB, - paymentsDBOptions..., - ) + + dbs.GraphDB, err = graphdb.NewChannelGraph(graphStore, chanGraphOpts...) if err != nil { cleanUp() - err = fmt.Errorf("unable to open payments DB: %w", err) + err = fmt.Errorf("unable to open channel graph DB: %w", err) d.logger.Error(err) return nil, nil, err } - dbs.PaymentsDB = kvPaymentsDB // Wrap the watchtower client DB and make sure we clean up. if cfg.WtClient.Active { diff --git a/config_prod.go b/config_prod.go index 60dba8bb50c..02b7d2aac8a 100644 --- a/config_prod.go +++ b/config_prod.go @@ -6,6 +6,8 @@ import ( "context" "github.com/lightningnetwork/lnd/kvdb" + paymentsdb "github.com/lightningnetwork/lnd/payments/db" + "github.com/lightningnetwork/lnd/sqldb" "github.com/lightningnetwork/lnd/sqldb/sqlc" ) @@ -24,3 +26,12 @@ func (d *DefaultDatabaseBuilder) getSQLMigration(ctx context.Context, return nil, false } + +// getPaymentsStore returns a paymentsdb.DB backed by a paymentsdb.KVStore +// implementation. +func (d *DefaultDatabaseBuilder) getPaymentsStore(_ *sqldb.BaseDB, + kvBackend kvdb.Backend, + opts ...paymentsdb.OptionModifier) (paymentsdb.DB, error) { + + return paymentsdb.NewKVStore(kvBackend, opts...) +} diff --git a/config_test_native_sql.go b/config_test_native_sql.go index 91589fa688e..efc6ed81e4a 100644 --- a/config_test_native_sql.go +++ b/config_test_native_sql.go @@ -4,8 +4,12 @@ package lnd import ( "context" + "database/sql" "github.com/lightningnetwork/lnd/kvdb" + "github.com/lightningnetwork/lnd/lncfg" + paymentsdb "github.com/lightningnetwork/lnd/payments/db" + "github.com/lightningnetwork/lnd/sqldb" "github.com/lightningnetwork/lnd/sqldb/sqlc" ) @@ -25,3 +29,28 @@ func (d *DefaultDatabaseBuilder) getSQLMigration(_ context.Context, return nil, false } } + +// getPaymentsStore returns a paymentsdb.DB backed by a paymentsdb.SQLStore +// implementation. +func (d *DefaultDatabaseBuilder) getPaymentsStore(baseDB *sqldb.BaseDB, + kvBackend kvdb.Backend, + opts ...paymentsdb.OptionModifier) (paymentsdb.DB, error) { + + paymentsExecutor := sqldb.NewTransactionExecutor( + baseDB, func(tx *sql.Tx) paymentsdb.SQLQueries { + return baseDB.WithTx(tx) + }, + ) + + queryConfig := d.cfg.DB.Sqlite.QueryConfig + if d.cfg.DB.Backend == lncfg.PostgresBackend { + queryConfig = d.cfg.DB.Postgres.QueryConfig + } + + return paymentsdb.NewSQLStore( + &paymentsdb.SQLStoreConfig{ + QueryCfg: &queryConfig, + }, + paymentsExecutor, opts..., + ) +} diff --git a/docs/release-notes/release-notes-0.21.0.md b/docs/release-notes/release-notes-0.21.0.md index 93892dbbe5e..89f316c9fbd 100644 --- a/docs/release-notes/release-notes-0.21.0.md +++ b/docs/release-notes/release-notes-0.21.0.md @@ -137,6 +137,32 @@ code](https://github.com/lightningnetwork/lnd/pull/10338) to prevent the need for maintenance as the sqlc code evolves. +* Payment Store SQL implementation and migration project: + * Introduce an [abstract payment + store](https://github.com/lightningnetwork/lnd/pull/10153) interface and + refacotor the payment related LND code to make it more modular. + * Implement the SQL backend for the [payments + database](https://github.com/lightningnetwork/lnd/pull/9147) + * Implement query methods (QueryPayments,FetchPayment) for the [payments db + SQL Backend](https://github.com/lightningnetwork/lnd/pull/10287) + * Implement insert methods for the [payments db + SQL Backend](https://github.com/lightningnetwork/lnd/pull/10291) + * Implement third(final) Part of SQL backend [payment + functions](https://github.com/lightningnetwork/lnd/pull/10368) + * Finalize SQL payments implementation [enabling unit and itests + for SQL backend](https://github.com/lightningnetwork/lnd/pull/10292) + * [Thread context through payment + db functions Part 1](https://github.com/lightningnetwork/lnd/pull/10307) + * [Thread context through payment + db functions Part 2](https://github.com/lightningnetwork/lnd/pull/10308) + * [Finalize SQL implementation for + payments db](https://github.com/lightningnetwork/lnd/pull/10373) + * [Add the KV-to-SQL payment + migration](https://github.com/lightningnetwork/lnd/pull/10485) with + comprehensive tests and build tag "test_native_sql" gated wiring into the + payment flow. + + ## Code Health ## Tooling and Documentation @@ -146,7 +172,9 @@ * Boris Nagaev * Elle Mouton * Erick Cestari +* Gijs van Dam * hieblmi * Mohamed Awnallah * Nishant Bansal * Pins +* Ziggie diff --git a/go.mod b/go.mod index 35a47c70253..a9b57fdc483 100644 --- a/go.mod +++ b/go.mod @@ -141,7 +141,7 @@ require ( github.com/opencontainers/runc v1.1.14 // indirect github.com/ory/dockertest/v3 v3.10.0 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.0 github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.26.0 // indirect github.com/prometheus/procfs v0.6.0 // indirect diff --git a/itest/lnd_payment_test.go b/itest/lnd_payment_test.go index 37aff052264..f683cd44a80 100644 --- a/itest/lnd_payment_test.go +++ b/itest/lnd_payment_test.go @@ -504,61 +504,86 @@ func testListPayments(ht *lntest.HarnessTest) { expected bool } - // Create test cases to check the timestamp filters. - createCases := func(createTimeSeconds uint64) []testCase { + // Create test cases with proper rounding for start and end dates. + createCases := func(startTimeSeconds, + endTimeSeconds uint64) []testCase { + return []testCase{ { // Use a start date same as the creation date - // should return us the item. + // (truncated) should return us the item. name: "exact start date", - startDate: createTimeSeconds, + startDate: startTimeSeconds, expected: true, }, { // Use an earlier start date should return us // the item. name: "earlier start date", - startDate: createTimeSeconds - 1, + startDate: startTimeSeconds - 1, expected: true, }, { // Use a future start date should return us // nothing. name: "future start date", - startDate: createTimeSeconds + 1, + startDate: startTimeSeconds + 1, expected: false, }, { // Use an end date same as the creation date - // should return us the item. + // (ceiling) should return us the item. name: "exact end date", - endDate: createTimeSeconds, + endDate: endTimeSeconds, expected: true, }, { // Use an end date in the future should return // us the item. name: "future end date", - endDate: createTimeSeconds + 1, + endDate: endTimeSeconds + 1, expected: true, }, { // Use an earlier end date should return us // nothing. - name: "earlier end date", - endDate: createTimeSeconds - 1, + name: "earlier end date", + // The native sql backend has a higher + // precision than the kv backend, the native sql + // backend uses microseconds, the kv backend + // when filtering uses seconds so we need to + // subtract 2 seconds to ensure the payment is + // not included. + // We could also truncate before inserting + // into the sql db but I rather relax this test + // here. + endDate: endTimeSeconds - 2, expected: false, }, } } - // Get the payment creation time in seconds. - paymentCreateSeconds := uint64( - p.CreationTimeNs / time.Second.Nanoseconds(), + // Get the payment creation time in seconds, using different approaches + // for start and end date comparisons to avoid rounding issues. + creationTime := time.Unix(0, p.CreationTimeNs) + + // For start date comparisons: use truncation (floor) to include + // payments from the beginning of that second. + paymentCreateSecondsStart := uint64( + creationTime.Truncate(time.Second).Unix(), + ) + + // For end date comparisons: use ceiling to include payments up to the + // end of that second. + paymentCreateSecondsEnd := uint64( + (p.CreationTimeNs + time.Second.Nanoseconds() - 1) / + time.Second.Nanoseconds(), ) // Create test cases from the payment creation time. - testCases := createCases(paymentCreateSeconds) + testCases := createCases( + paymentCreateSecondsStart, paymentCreateSecondsEnd, + ) // We now check the timestamp filters in `ListPayments`. for _, tc := range testCases { @@ -578,7 +603,9 @@ func testListPayments(ht *lntest.HarnessTest) { } // Create test cases from the invoice creation time. - testCases = createCases(uint64(invoice.CreationDate)) + testCases = createCases( + uint64(invoice.CreationDate), uint64(invoice.CreationDate), + ) // We now do the same check for `ListInvoices`. for _, tc := range testCases { diff --git a/lnrpc/lightning.pb.go b/lnrpc/lightning.pb.go index b30c107df97..826f5c78688 100644 --- a/lnrpc/lightning.pb.go +++ b/lnrpc/lightning.pb.go @@ -1382,7 +1382,7 @@ func (x Failure_FailureCode) Number() protoreflect.EnumNumber { // Deprecated: Use Failure_FailureCode.Descriptor instead. func (Failure_FailureCode) EnumDescriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{195, 0} + return file_lightning_proto_rawDescGZIP(), []int{200, 0} } type LookupHtlcResolutionRequest struct { @@ -14722,6 +14722,281 @@ func (x *ListPaymentsResponse) GetTotalNumPayments() uint64 { return 0 } +type ListPaymentDuplicatesRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The payment hash whose duplicates should be returned. + PaymentHash []byte `protobuf:"bytes,1,opt,name=payment_hash,json=paymentHash,proto3" json:"payment_hash,omitempty"` +} + +func (x *ListPaymentDuplicatesRequest) Reset() { + *x = ListPaymentDuplicatesRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_lightning_proto_msgTypes[154] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListPaymentDuplicatesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListPaymentDuplicatesRequest) ProtoMessage() {} + +func (x *ListPaymentDuplicatesRequest) ProtoReflect() protoreflect.Message { + mi := &file_lightning_proto_msgTypes[154] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListPaymentDuplicatesRequest.ProtoReflect.Descriptor instead. +func (*ListPaymentDuplicatesRequest) Descriptor() ([]byte, []int) { + return file_lightning_proto_rawDescGZIP(), []int{154} +} + +func (x *ListPaymentDuplicatesRequest) GetPaymentHash() []byte { + if x != nil { + return x.PaymentHash + } + return nil +} + +type ListPaymentDuplicatesResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The list of duplicate payment records for the given payment. + Duplicates []*PaymentDuplicate `protobuf:"bytes,1,rep,name=duplicates,proto3" json:"duplicates,omitempty"` +} + +func (x *ListPaymentDuplicatesResponse) Reset() { + *x = ListPaymentDuplicatesResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_lightning_proto_msgTypes[155] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListPaymentDuplicatesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListPaymentDuplicatesResponse) ProtoMessage() {} + +func (x *ListPaymentDuplicatesResponse) ProtoReflect() protoreflect.Message { + mi := &file_lightning_proto_msgTypes[155] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListPaymentDuplicatesResponse.ProtoReflect.Descriptor instead. +func (*ListPaymentDuplicatesResponse) Descriptor() ([]byte, []int) { + return file_lightning_proto_rawDescGZIP(), []int{155} +} + +func (x *ListPaymentDuplicatesResponse) GetDuplicates() []*PaymentDuplicate { + if x != nil { + return x.Duplicates + } + return nil +} + +type ListAllPaymentDuplicatesRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *ListAllPaymentDuplicatesRequest) Reset() { + *x = ListAllPaymentDuplicatesRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_lightning_proto_msgTypes[156] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListAllPaymentDuplicatesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListAllPaymentDuplicatesRequest) ProtoMessage() {} + +func (x *ListAllPaymentDuplicatesRequest) ProtoReflect() protoreflect.Message { + mi := &file_lightning_proto_msgTypes[156] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListAllPaymentDuplicatesRequest.ProtoReflect.Descriptor instead. +func (*ListAllPaymentDuplicatesRequest) Descriptor() ([]byte, []int) { + return file_lightning_proto_rawDescGZIP(), []int{156} +} + +type ListAllPaymentDuplicatesResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The list of duplicate payment records across all payments. + Duplicates []*PaymentDuplicate `protobuf:"bytes,1,rep,name=duplicates,proto3" json:"duplicates,omitempty"` +} + +func (x *ListAllPaymentDuplicatesResponse) Reset() { + *x = ListAllPaymentDuplicatesResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_lightning_proto_msgTypes[157] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListAllPaymentDuplicatesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListAllPaymentDuplicatesResponse) ProtoMessage() {} + +func (x *ListAllPaymentDuplicatesResponse) ProtoReflect() protoreflect.Message { + mi := &file_lightning_proto_msgTypes[157] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListAllPaymentDuplicatesResponse.ProtoReflect.Descriptor instead. +func (*ListAllPaymentDuplicatesResponse) Descriptor() ([]byte, []int) { + return file_lightning_proto_rawDescGZIP(), []int{157} +} + +func (x *ListAllPaymentDuplicatesResponse) GetDuplicates() []*PaymentDuplicate { + if x != nil { + return x.Duplicates + } + return nil +} + +type PaymentDuplicate struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The payment hash for the duplicate payment. + PaymentHash []byte `protobuf:"bytes,1,opt,name=payment_hash,json=paymentHash,proto3" json:"payment_hash,omitempty"` + // The value of the duplicate payment in milli-satoshis. + ValueMsat int64 `protobuf:"varint,2,opt,name=value_msat,json=valueMsat,proto3" json:"value_msat,omitempty"` + // The time in UNIX nanoseconds at which the duplicate was created. + CreationTimeNs int64 `protobuf:"varint,3,opt,name=creation_time_ns,json=creationTimeNs,proto3" json:"creation_time_ns,omitempty"` + // The failure reason for failed duplicates. + FailureReason PaymentFailureReason `protobuf:"varint,4,opt,name=failure_reason,json=failureReason,proto3,enum=lnrpc.PaymentFailureReason" json:"failure_reason,omitempty"` + // The payment preimage for settled duplicates. + PaymentPreimage []byte `protobuf:"bytes,5,opt,name=payment_preimage,json=paymentPreimage,proto3" json:"payment_preimage,omitempty"` + // The time in UNIX nanoseconds at which the duplicate was settled. + SettleTimeNs int64 `protobuf:"varint,6,opt,name=settle_time_ns,json=settleTimeNs,proto3" json:"settle_time_ns,omitempty"` +} + +func (x *PaymentDuplicate) Reset() { + *x = PaymentDuplicate{} + if protoimpl.UnsafeEnabled { + mi := &file_lightning_proto_msgTypes[158] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PaymentDuplicate) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PaymentDuplicate) ProtoMessage() {} + +func (x *PaymentDuplicate) ProtoReflect() protoreflect.Message { + mi := &file_lightning_proto_msgTypes[158] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PaymentDuplicate.ProtoReflect.Descriptor instead. +func (*PaymentDuplicate) Descriptor() ([]byte, []int) { + return file_lightning_proto_rawDescGZIP(), []int{158} +} + +func (x *PaymentDuplicate) GetPaymentHash() []byte { + if x != nil { + return x.PaymentHash + } + return nil +} + +func (x *PaymentDuplicate) GetValueMsat() int64 { + if x != nil { + return x.ValueMsat + } + return 0 +} + +func (x *PaymentDuplicate) GetCreationTimeNs() int64 { + if x != nil { + return x.CreationTimeNs + } + return 0 +} + +func (x *PaymentDuplicate) GetFailureReason() PaymentFailureReason { + if x != nil { + return x.FailureReason + } + return PaymentFailureReason_FAILURE_REASON_NONE +} + +func (x *PaymentDuplicate) GetPaymentPreimage() []byte { + if x != nil { + return x.PaymentPreimage + } + return nil +} + +func (x *PaymentDuplicate) GetSettleTimeNs() int64 { + if x != nil { + return x.SettleTimeNs + } + return 0 +} + type DeletePaymentRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -14736,7 +15011,7 @@ type DeletePaymentRequest struct { func (x *DeletePaymentRequest) Reset() { *x = DeletePaymentRequest{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[154] + mi := &file_lightning_proto_msgTypes[159] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14749,7 +15024,7 @@ func (x *DeletePaymentRequest) String() string { func (*DeletePaymentRequest) ProtoMessage() {} func (x *DeletePaymentRequest) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[154] + mi := &file_lightning_proto_msgTypes[159] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14762,7 +15037,7 @@ func (x *DeletePaymentRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeletePaymentRequest.ProtoReflect.Descriptor instead. func (*DeletePaymentRequest) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{154} + return file_lightning_proto_rawDescGZIP(), []int{159} } func (x *DeletePaymentRequest) GetPaymentHash() []byte { @@ -14796,7 +15071,7 @@ type DeleteAllPaymentsRequest struct { func (x *DeleteAllPaymentsRequest) Reset() { *x = DeleteAllPaymentsRequest{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[155] + mi := &file_lightning_proto_msgTypes[160] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14809,7 +15084,7 @@ func (x *DeleteAllPaymentsRequest) String() string { func (*DeleteAllPaymentsRequest) ProtoMessage() {} func (x *DeleteAllPaymentsRequest) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[155] + mi := &file_lightning_proto_msgTypes[160] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14822,7 +15097,7 @@ func (x *DeleteAllPaymentsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteAllPaymentsRequest.ProtoReflect.Descriptor instead. func (*DeleteAllPaymentsRequest) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{155} + return file_lightning_proto_rawDescGZIP(), []int{160} } func (x *DeleteAllPaymentsRequest) GetFailedPaymentsOnly() bool { @@ -14858,7 +15133,7 @@ type DeletePaymentResponse struct { func (x *DeletePaymentResponse) Reset() { *x = DeletePaymentResponse{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[156] + mi := &file_lightning_proto_msgTypes[161] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14871,7 +15146,7 @@ func (x *DeletePaymentResponse) String() string { func (*DeletePaymentResponse) ProtoMessage() {} func (x *DeletePaymentResponse) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[156] + mi := &file_lightning_proto_msgTypes[161] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14884,7 +15159,7 @@ func (x *DeletePaymentResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeletePaymentResponse.ProtoReflect.Descriptor instead. func (*DeletePaymentResponse) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{156} + return file_lightning_proto_rawDescGZIP(), []int{161} } func (x *DeletePaymentResponse) GetStatus() string { @@ -14906,7 +15181,7 @@ type DeleteAllPaymentsResponse struct { func (x *DeleteAllPaymentsResponse) Reset() { *x = DeleteAllPaymentsResponse{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[157] + mi := &file_lightning_proto_msgTypes[162] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14919,7 +15194,7 @@ func (x *DeleteAllPaymentsResponse) String() string { func (*DeleteAllPaymentsResponse) ProtoMessage() {} func (x *DeleteAllPaymentsResponse) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[157] + mi := &file_lightning_proto_msgTypes[162] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14932,7 +15207,7 @@ func (x *DeleteAllPaymentsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteAllPaymentsResponse.ProtoReflect.Descriptor instead. func (*DeleteAllPaymentsResponse) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{157} + return file_lightning_proto_rawDescGZIP(), []int{162} } func (x *DeleteAllPaymentsResponse) GetStatus() string { @@ -14958,7 +15233,7 @@ type AbandonChannelRequest struct { func (x *AbandonChannelRequest) Reset() { *x = AbandonChannelRequest{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[158] + mi := &file_lightning_proto_msgTypes[163] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14971,7 +15246,7 @@ func (x *AbandonChannelRequest) String() string { func (*AbandonChannelRequest) ProtoMessage() {} func (x *AbandonChannelRequest) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[158] + mi := &file_lightning_proto_msgTypes[163] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14984,7 +15259,7 @@ func (x *AbandonChannelRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AbandonChannelRequest.ProtoReflect.Descriptor instead. func (*AbandonChannelRequest) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{158} + return file_lightning_proto_rawDescGZIP(), []int{163} } func (x *AbandonChannelRequest) GetChannelPoint() *ChannelPoint { @@ -15020,7 +15295,7 @@ type AbandonChannelResponse struct { func (x *AbandonChannelResponse) Reset() { *x = AbandonChannelResponse{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[159] + mi := &file_lightning_proto_msgTypes[164] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15033,7 +15308,7 @@ func (x *AbandonChannelResponse) String() string { func (*AbandonChannelResponse) ProtoMessage() {} func (x *AbandonChannelResponse) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[159] + mi := &file_lightning_proto_msgTypes[164] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15046,7 +15321,7 @@ func (x *AbandonChannelResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use AbandonChannelResponse.ProtoReflect.Descriptor instead. func (*AbandonChannelResponse) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{159} + return file_lightning_proto_rawDescGZIP(), []int{164} } func (x *AbandonChannelResponse) GetStatus() string { @@ -15068,7 +15343,7 @@ type DebugLevelRequest struct { func (x *DebugLevelRequest) Reset() { *x = DebugLevelRequest{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[160] + mi := &file_lightning_proto_msgTypes[165] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15081,7 +15356,7 @@ func (x *DebugLevelRequest) String() string { func (*DebugLevelRequest) ProtoMessage() {} func (x *DebugLevelRequest) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[160] + mi := &file_lightning_proto_msgTypes[165] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15094,7 +15369,7 @@ func (x *DebugLevelRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DebugLevelRequest.ProtoReflect.Descriptor instead. func (*DebugLevelRequest) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{160} + return file_lightning_proto_rawDescGZIP(), []int{165} } func (x *DebugLevelRequest) GetShow() bool { @@ -15122,7 +15397,7 @@ type DebugLevelResponse struct { func (x *DebugLevelResponse) Reset() { *x = DebugLevelResponse{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[161] + mi := &file_lightning_proto_msgTypes[166] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15135,7 +15410,7 @@ func (x *DebugLevelResponse) String() string { func (*DebugLevelResponse) ProtoMessage() {} func (x *DebugLevelResponse) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[161] + mi := &file_lightning_proto_msgTypes[166] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15148,7 +15423,7 @@ func (x *DebugLevelResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DebugLevelResponse.ProtoReflect.Descriptor instead. func (*DebugLevelResponse) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{161} + return file_lightning_proto_rawDescGZIP(), []int{166} } func (x *DebugLevelResponse) GetSubSystems() string { @@ -15170,7 +15445,7 @@ type PayReqString struct { func (x *PayReqString) Reset() { *x = PayReqString{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[162] + mi := &file_lightning_proto_msgTypes[167] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15183,7 +15458,7 @@ func (x *PayReqString) String() string { func (*PayReqString) ProtoMessage() {} func (x *PayReqString) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[162] + mi := &file_lightning_proto_msgTypes[167] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15196,7 +15471,7 @@ func (x *PayReqString) ProtoReflect() protoreflect.Message { // Deprecated: Use PayReqString.ProtoReflect.Descriptor instead. func (*PayReqString) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{162} + return file_lightning_proto_rawDescGZIP(), []int{167} } func (x *PayReqString) GetPayReq() string { @@ -15230,7 +15505,7 @@ type PayReq struct { func (x *PayReq) Reset() { *x = PayReq{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[163] + mi := &file_lightning_proto_msgTypes[168] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15243,7 +15518,7 @@ func (x *PayReq) String() string { func (*PayReq) ProtoMessage() {} func (x *PayReq) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[163] + mi := &file_lightning_proto_msgTypes[168] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15256,7 +15531,7 @@ func (x *PayReq) ProtoReflect() protoreflect.Message { // Deprecated: Use PayReq.ProtoReflect.Descriptor instead. func (*PayReq) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{163} + return file_lightning_proto_rawDescGZIP(), []int{168} } func (x *PayReq) GetDestination() string { @@ -15370,7 +15645,7 @@ type Feature struct { func (x *Feature) Reset() { *x = Feature{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[164] + mi := &file_lightning_proto_msgTypes[169] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15383,7 +15658,7 @@ func (x *Feature) String() string { func (*Feature) ProtoMessage() {} func (x *Feature) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[164] + mi := &file_lightning_proto_msgTypes[169] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15396,7 +15671,7 @@ func (x *Feature) ProtoReflect() protoreflect.Message { // Deprecated: Use Feature.ProtoReflect.Descriptor instead. func (*Feature) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{164} + return file_lightning_proto_rawDescGZIP(), []int{169} } func (x *Feature) GetName() string { @@ -15429,7 +15704,7 @@ type FeeReportRequest struct { func (x *FeeReportRequest) Reset() { *x = FeeReportRequest{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[165] + mi := &file_lightning_proto_msgTypes[170] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15442,7 +15717,7 @@ func (x *FeeReportRequest) String() string { func (*FeeReportRequest) ProtoMessage() {} func (x *FeeReportRequest) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[165] + mi := &file_lightning_proto_msgTypes[170] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15455,7 +15730,7 @@ func (x *FeeReportRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FeeReportRequest.ProtoReflect.Descriptor instead. func (*FeeReportRequest) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{165} + return file_lightning_proto_rawDescGZIP(), []int{170} } type ChannelFeeReport struct { @@ -15485,7 +15760,7 @@ type ChannelFeeReport struct { func (x *ChannelFeeReport) Reset() { *x = ChannelFeeReport{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[166] + mi := &file_lightning_proto_msgTypes[171] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15498,7 +15773,7 @@ func (x *ChannelFeeReport) String() string { func (*ChannelFeeReport) ProtoMessage() {} func (x *ChannelFeeReport) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[166] + mi := &file_lightning_proto_msgTypes[171] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15511,7 +15786,7 @@ func (x *ChannelFeeReport) ProtoReflect() protoreflect.Message { // Deprecated: Use ChannelFeeReport.ProtoReflect.Descriptor instead. func (*ChannelFeeReport) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{166} + return file_lightning_proto_rawDescGZIP(), []int{171} } func (x *ChannelFeeReport) GetChanId() uint64 { @@ -15585,7 +15860,7 @@ type FeeReportResponse struct { func (x *FeeReportResponse) Reset() { *x = FeeReportResponse{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[167] + mi := &file_lightning_proto_msgTypes[172] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15598,7 +15873,7 @@ func (x *FeeReportResponse) String() string { func (*FeeReportResponse) ProtoMessage() {} func (x *FeeReportResponse) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[167] + mi := &file_lightning_proto_msgTypes[172] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15611,7 +15886,7 @@ func (x *FeeReportResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use FeeReportResponse.ProtoReflect.Descriptor instead. func (*FeeReportResponse) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{167} + return file_lightning_proto_rawDescGZIP(), []int{172} } func (x *FeeReportResponse) GetChannelFees() []*ChannelFeeReport { @@ -15658,7 +15933,7 @@ type InboundFee struct { func (x *InboundFee) Reset() { *x = InboundFee{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[168] + mi := &file_lightning_proto_msgTypes[173] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15671,7 +15946,7 @@ func (x *InboundFee) String() string { func (*InboundFee) ProtoMessage() {} func (x *InboundFee) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[168] + mi := &file_lightning_proto_msgTypes[173] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15684,7 +15959,7 @@ func (x *InboundFee) ProtoReflect() protoreflect.Message { // Deprecated: Use InboundFee.ProtoReflect.Descriptor instead. func (*InboundFee) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{168} + return file_lightning_proto_rawDescGZIP(), []int{173} } func (x *InboundFee) GetBaseFeeMsat() int32 { @@ -15744,7 +16019,7 @@ type PolicyUpdateRequest struct { func (x *PolicyUpdateRequest) Reset() { *x = PolicyUpdateRequest{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[169] + mi := &file_lightning_proto_msgTypes[174] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15757,7 +16032,7 @@ func (x *PolicyUpdateRequest) String() string { func (*PolicyUpdateRequest) ProtoMessage() {} func (x *PolicyUpdateRequest) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[169] + mi := &file_lightning_proto_msgTypes[174] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15770,7 +16045,7 @@ func (x *PolicyUpdateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PolicyUpdateRequest.ProtoReflect.Descriptor instead. func (*PolicyUpdateRequest) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{169} + return file_lightning_proto_rawDescGZIP(), []int{174} } func (m *PolicyUpdateRequest) GetScope() isPolicyUpdateRequest_Scope { @@ -15891,7 +16166,7 @@ type FailedUpdate struct { func (x *FailedUpdate) Reset() { *x = FailedUpdate{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[170] + mi := &file_lightning_proto_msgTypes[175] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15904,7 +16179,7 @@ func (x *FailedUpdate) String() string { func (*FailedUpdate) ProtoMessage() {} func (x *FailedUpdate) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[170] + mi := &file_lightning_proto_msgTypes[175] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15917,7 +16192,7 @@ func (x *FailedUpdate) ProtoReflect() protoreflect.Message { // Deprecated: Use FailedUpdate.ProtoReflect.Descriptor instead. func (*FailedUpdate) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{170} + return file_lightning_proto_rawDescGZIP(), []int{175} } func (x *FailedUpdate) GetOutpoint() *OutPoint { @@ -15953,7 +16228,7 @@ type PolicyUpdateResponse struct { func (x *PolicyUpdateResponse) Reset() { *x = PolicyUpdateResponse{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[171] + mi := &file_lightning_proto_msgTypes[176] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15966,7 +16241,7 @@ func (x *PolicyUpdateResponse) String() string { func (*PolicyUpdateResponse) ProtoMessage() {} func (x *PolicyUpdateResponse) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[171] + mi := &file_lightning_proto_msgTypes[176] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15979,7 +16254,7 @@ func (x *PolicyUpdateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PolicyUpdateResponse.ProtoReflect.Descriptor instead. func (*PolicyUpdateResponse) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{171} + return file_lightning_proto_rawDescGZIP(), []int{176} } func (x *PolicyUpdateResponse) GetFailedUpdates() []*FailedUpdate { @@ -16022,7 +16297,7 @@ type ForwardingHistoryRequest struct { func (x *ForwardingHistoryRequest) Reset() { *x = ForwardingHistoryRequest{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[172] + mi := &file_lightning_proto_msgTypes[177] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16035,7 +16310,7 @@ func (x *ForwardingHistoryRequest) String() string { func (*ForwardingHistoryRequest) ProtoMessage() {} func (x *ForwardingHistoryRequest) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[172] + mi := &file_lightning_proto_msgTypes[177] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16048,7 +16323,7 @@ func (x *ForwardingHistoryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ForwardingHistoryRequest.ProtoReflect.Descriptor instead. func (*ForwardingHistoryRequest) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{172} + return file_lightning_proto_rawDescGZIP(), []int{177} } func (x *ForwardingHistoryRequest) GetStartTime() uint64 { @@ -16149,7 +16424,7 @@ type ForwardingEvent struct { func (x *ForwardingEvent) Reset() { *x = ForwardingEvent{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[173] + mi := &file_lightning_proto_msgTypes[178] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16162,7 +16437,7 @@ func (x *ForwardingEvent) String() string { func (*ForwardingEvent) ProtoMessage() {} func (x *ForwardingEvent) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[173] + mi := &file_lightning_proto_msgTypes[178] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16175,7 +16450,7 @@ func (x *ForwardingEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use ForwardingEvent.ProtoReflect.Descriptor instead. func (*ForwardingEvent) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{173} + return file_lightning_proto_rawDescGZIP(), []int{178} } // Deprecated: Marked as deprecated in lightning.proto. @@ -16293,7 +16568,7 @@ type ForwardingHistoryResponse struct { func (x *ForwardingHistoryResponse) Reset() { *x = ForwardingHistoryResponse{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[174] + mi := &file_lightning_proto_msgTypes[179] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16306,7 +16581,7 @@ func (x *ForwardingHistoryResponse) String() string { func (*ForwardingHistoryResponse) ProtoMessage() {} func (x *ForwardingHistoryResponse) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[174] + mi := &file_lightning_proto_msgTypes[179] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16319,7 +16594,7 @@ func (x *ForwardingHistoryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ForwardingHistoryResponse.ProtoReflect.Descriptor instead. func (*ForwardingHistoryResponse) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{174} + return file_lightning_proto_rawDescGZIP(), []int{179} } func (x *ForwardingHistoryResponse) GetForwardingEvents() []*ForwardingEvent { @@ -16348,7 +16623,7 @@ type ExportChannelBackupRequest struct { func (x *ExportChannelBackupRequest) Reset() { *x = ExportChannelBackupRequest{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[175] + mi := &file_lightning_proto_msgTypes[180] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16361,7 +16636,7 @@ func (x *ExportChannelBackupRequest) String() string { func (*ExportChannelBackupRequest) ProtoMessage() {} func (x *ExportChannelBackupRequest) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[175] + mi := &file_lightning_proto_msgTypes[180] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16374,7 +16649,7 @@ func (x *ExportChannelBackupRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ExportChannelBackupRequest.ProtoReflect.Descriptor instead. func (*ExportChannelBackupRequest) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{175} + return file_lightning_proto_rawDescGZIP(), []int{180} } func (x *ExportChannelBackupRequest) GetChanPoint() *ChannelPoint { @@ -16401,7 +16676,7 @@ type ChannelBackup struct { func (x *ChannelBackup) Reset() { *x = ChannelBackup{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[176] + mi := &file_lightning_proto_msgTypes[181] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16414,7 +16689,7 @@ func (x *ChannelBackup) String() string { func (*ChannelBackup) ProtoMessage() {} func (x *ChannelBackup) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[176] + mi := &file_lightning_proto_msgTypes[181] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16427,7 +16702,7 @@ func (x *ChannelBackup) ProtoReflect() protoreflect.Message { // Deprecated: Use ChannelBackup.ProtoReflect.Descriptor instead. func (*ChannelBackup) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{176} + return file_lightning_proto_rawDescGZIP(), []int{181} } func (x *ChannelBackup) GetChanPoint() *ChannelPoint { @@ -16461,7 +16736,7 @@ type MultiChanBackup struct { func (x *MultiChanBackup) Reset() { *x = MultiChanBackup{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[177] + mi := &file_lightning_proto_msgTypes[182] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16474,7 +16749,7 @@ func (x *MultiChanBackup) String() string { func (*MultiChanBackup) ProtoMessage() {} func (x *MultiChanBackup) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[177] + mi := &file_lightning_proto_msgTypes[182] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16487,7 +16762,7 @@ func (x *MultiChanBackup) ProtoReflect() protoreflect.Message { // Deprecated: Use MultiChanBackup.ProtoReflect.Descriptor instead. func (*MultiChanBackup) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{177} + return file_lightning_proto_rawDescGZIP(), []int{182} } func (x *MultiChanBackup) GetChanPoints() []*ChannelPoint { @@ -16513,7 +16788,7 @@ type ChanBackupExportRequest struct { func (x *ChanBackupExportRequest) Reset() { *x = ChanBackupExportRequest{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[178] + mi := &file_lightning_proto_msgTypes[183] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16526,7 +16801,7 @@ func (x *ChanBackupExportRequest) String() string { func (*ChanBackupExportRequest) ProtoMessage() {} func (x *ChanBackupExportRequest) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[178] + mi := &file_lightning_proto_msgTypes[183] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16539,7 +16814,7 @@ func (x *ChanBackupExportRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ChanBackupExportRequest.ProtoReflect.Descriptor instead. func (*ChanBackupExportRequest) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{178} + return file_lightning_proto_rawDescGZIP(), []int{183} } type ChanBackupSnapshot struct { @@ -16558,7 +16833,7 @@ type ChanBackupSnapshot struct { func (x *ChanBackupSnapshot) Reset() { *x = ChanBackupSnapshot{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[179] + mi := &file_lightning_proto_msgTypes[184] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16571,7 +16846,7 @@ func (x *ChanBackupSnapshot) String() string { func (*ChanBackupSnapshot) ProtoMessage() {} func (x *ChanBackupSnapshot) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[179] + mi := &file_lightning_proto_msgTypes[184] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16584,7 +16859,7 @@ func (x *ChanBackupSnapshot) ProtoReflect() protoreflect.Message { // Deprecated: Use ChanBackupSnapshot.ProtoReflect.Descriptor instead. func (*ChanBackupSnapshot) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{179} + return file_lightning_proto_rawDescGZIP(), []int{184} } func (x *ChanBackupSnapshot) GetSingleChanBackups() *ChannelBackups { @@ -16613,7 +16888,7 @@ type ChannelBackups struct { func (x *ChannelBackups) Reset() { *x = ChannelBackups{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[180] + mi := &file_lightning_proto_msgTypes[185] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16626,7 +16901,7 @@ func (x *ChannelBackups) String() string { func (*ChannelBackups) ProtoMessage() {} func (x *ChannelBackups) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[180] + mi := &file_lightning_proto_msgTypes[185] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16639,7 +16914,7 @@ func (x *ChannelBackups) ProtoReflect() protoreflect.Message { // Deprecated: Use ChannelBackups.ProtoReflect.Descriptor instead. func (*ChannelBackups) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{180} + return file_lightning_proto_rawDescGZIP(), []int{185} } func (x *ChannelBackups) GetChanBackups() []*ChannelBackup { @@ -16664,7 +16939,7 @@ type RestoreChanBackupRequest struct { func (x *RestoreChanBackupRequest) Reset() { *x = RestoreChanBackupRequest{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[181] + mi := &file_lightning_proto_msgTypes[186] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16677,7 +16952,7 @@ func (x *RestoreChanBackupRequest) String() string { func (*RestoreChanBackupRequest) ProtoMessage() {} func (x *RestoreChanBackupRequest) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[181] + mi := &file_lightning_proto_msgTypes[186] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16690,7 +16965,7 @@ func (x *RestoreChanBackupRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RestoreChanBackupRequest.ProtoReflect.Descriptor instead. func (*RestoreChanBackupRequest) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{181} + return file_lightning_proto_rawDescGZIP(), []int{186} } func (m *RestoreChanBackupRequest) GetBackup() isRestoreChanBackupRequest_Backup { @@ -16745,7 +17020,7 @@ type RestoreBackupResponse struct { func (x *RestoreBackupResponse) Reset() { *x = RestoreBackupResponse{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[182] + mi := &file_lightning_proto_msgTypes[187] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16758,7 +17033,7 @@ func (x *RestoreBackupResponse) String() string { func (*RestoreBackupResponse) ProtoMessage() {} func (x *RestoreBackupResponse) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[182] + mi := &file_lightning_proto_msgTypes[187] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16771,7 +17046,7 @@ func (x *RestoreBackupResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RestoreBackupResponse.ProtoReflect.Descriptor instead. func (*RestoreBackupResponse) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{182} + return file_lightning_proto_rawDescGZIP(), []int{187} } func (x *RestoreBackupResponse) GetNumRestored() uint32 { @@ -16790,7 +17065,7 @@ type ChannelBackupSubscription struct { func (x *ChannelBackupSubscription) Reset() { *x = ChannelBackupSubscription{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[183] + mi := &file_lightning_proto_msgTypes[188] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16803,7 +17078,7 @@ func (x *ChannelBackupSubscription) String() string { func (*ChannelBackupSubscription) ProtoMessage() {} func (x *ChannelBackupSubscription) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[183] + mi := &file_lightning_proto_msgTypes[188] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16816,7 +17091,7 @@ func (x *ChannelBackupSubscription) ProtoReflect() protoreflect.Message { // Deprecated: Use ChannelBackupSubscription.ProtoReflect.Descriptor instead. func (*ChannelBackupSubscription) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{183} + return file_lightning_proto_rawDescGZIP(), []int{188} } type VerifyChanBackupResponse struct { @@ -16830,7 +17105,7 @@ type VerifyChanBackupResponse struct { func (x *VerifyChanBackupResponse) Reset() { *x = VerifyChanBackupResponse{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[184] + mi := &file_lightning_proto_msgTypes[189] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16843,7 +17118,7 @@ func (x *VerifyChanBackupResponse) String() string { func (*VerifyChanBackupResponse) ProtoMessage() {} func (x *VerifyChanBackupResponse) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[184] + mi := &file_lightning_proto_msgTypes[189] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16856,7 +17131,7 @@ func (x *VerifyChanBackupResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use VerifyChanBackupResponse.ProtoReflect.Descriptor instead. func (*VerifyChanBackupResponse) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{184} + return file_lightning_proto_rawDescGZIP(), []int{189} } func (x *VerifyChanBackupResponse) GetChanPoints() []string { @@ -16880,7 +17155,7 @@ type MacaroonPermission struct { func (x *MacaroonPermission) Reset() { *x = MacaroonPermission{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[185] + mi := &file_lightning_proto_msgTypes[190] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16893,7 +17168,7 @@ func (x *MacaroonPermission) String() string { func (*MacaroonPermission) ProtoMessage() {} func (x *MacaroonPermission) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[185] + mi := &file_lightning_proto_msgTypes[190] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16906,7 +17181,7 @@ func (x *MacaroonPermission) ProtoReflect() protoreflect.Message { // Deprecated: Use MacaroonPermission.ProtoReflect.Descriptor instead. func (*MacaroonPermission) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{185} + return file_lightning_proto_rawDescGZIP(), []int{190} } func (x *MacaroonPermission) GetEntity() string { @@ -16940,7 +17215,7 @@ type BakeMacaroonRequest struct { func (x *BakeMacaroonRequest) Reset() { *x = BakeMacaroonRequest{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[186] + mi := &file_lightning_proto_msgTypes[191] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16953,7 +17228,7 @@ func (x *BakeMacaroonRequest) String() string { func (*BakeMacaroonRequest) ProtoMessage() {} func (x *BakeMacaroonRequest) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[186] + mi := &file_lightning_proto_msgTypes[191] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16966,7 +17241,7 @@ func (x *BakeMacaroonRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use BakeMacaroonRequest.ProtoReflect.Descriptor instead. func (*BakeMacaroonRequest) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{186} + return file_lightning_proto_rawDescGZIP(), []int{191} } func (x *BakeMacaroonRequest) GetPermissions() []*MacaroonPermission { @@ -17002,7 +17277,7 @@ type BakeMacaroonResponse struct { func (x *BakeMacaroonResponse) Reset() { *x = BakeMacaroonResponse{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[187] + mi := &file_lightning_proto_msgTypes[192] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17015,7 +17290,7 @@ func (x *BakeMacaroonResponse) String() string { func (*BakeMacaroonResponse) ProtoMessage() {} func (x *BakeMacaroonResponse) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[187] + mi := &file_lightning_proto_msgTypes[192] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17028,7 +17303,7 @@ func (x *BakeMacaroonResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use BakeMacaroonResponse.ProtoReflect.Descriptor instead. func (*BakeMacaroonResponse) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{187} + return file_lightning_proto_rawDescGZIP(), []int{192} } func (x *BakeMacaroonResponse) GetMacaroon() string { @@ -17047,7 +17322,7 @@ type ListMacaroonIDsRequest struct { func (x *ListMacaroonIDsRequest) Reset() { *x = ListMacaroonIDsRequest{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[188] + mi := &file_lightning_proto_msgTypes[193] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17060,7 +17335,7 @@ func (x *ListMacaroonIDsRequest) String() string { func (*ListMacaroonIDsRequest) ProtoMessage() {} func (x *ListMacaroonIDsRequest) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[188] + mi := &file_lightning_proto_msgTypes[193] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17073,7 +17348,7 @@ func (x *ListMacaroonIDsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListMacaroonIDsRequest.ProtoReflect.Descriptor instead. func (*ListMacaroonIDsRequest) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{188} + return file_lightning_proto_rawDescGZIP(), []int{193} } type ListMacaroonIDsResponse struct { @@ -17088,7 +17363,7 @@ type ListMacaroonIDsResponse struct { func (x *ListMacaroonIDsResponse) Reset() { *x = ListMacaroonIDsResponse{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[189] + mi := &file_lightning_proto_msgTypes[194] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17101,7 +17376,7 @@ func (x *ListMacaroonIDsResponse) String() string { func (*ListMacaroonIDsResponse) ProtoMessage() {} func (x *ListMacaroonIDsResponse) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[189] + mi := &file_lightning_proto_msgTypes[194] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17114,7 +17389,7 @@ func (x *ListMacaroonIDsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListMacaroonIDsResponse.ProtoReflect.Descriptor instead. func (*ListMacaroonIDsResponse) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{189} + return file_lightning_proto_rawDescGZIP(), []int{194} } func (x *ListMacaroonIDsResponse) GetRootKeyIds() []uint64 { @@ -17136,7 +17411,7 @@ type DeleteMacaroonIDRequest struct { func (x *DeleteMacaroonIDRequest) Reset() { *x = DeleteMacaroonIDRequest{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[190] + mi := &file_lightning_proto_msgTypes[195] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17149,7 +17424,7 @@ func (x *DeleteMacaroonIDRequest) String() string { func (*DeleteMacaroonIDRequest) ProtoMessage() {} func (x *DeleteMacaroonIDRequest) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[190] + mi := &file_lightning_proto_msgTypes[195] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17162,7 +17437,7 @@ func (x *DeleteMacaroonIDRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteMacaroonIDRequest.ProtoReflect.Descriptor instead. func (*DeleteMacaroonIDRequest) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{190} + return file_lightning_proto_rawDescGZIP(), []int{195} } func (x *DeleteMacaroonIDRequest) GetRootKeyId() uint64 { @@ -17184,7 +17459,7 @@ type DeleteMacaroonIDResponse struct { func (x *DeleteMacaroonIDResponse) Reset() { *x = DeleteMacaroonIDResponse{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[191] + mi := &file_lightning_proto_msgTypes[196] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17197,7 +17472,7 @@ func (x *DeleteMacaroonIDResponse) String() string { func (*DeleteMacaroonIDResponse) ProtoMessage() {} func (x *DeleteMacaroonIDResponse) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[191] + mi := &file_lightning_proto_msgTypes[196] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17210,7 +17485,7 @@ func (x *DeleteMacaroonIDResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteMacaroonIDResponse.ProtoReflect.Descriptor instead. func (*DeleteMacaroonIDResponse) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{191} + return file_lightning_proto_rawDescGZIP(), []int{196} } func (x *DeleteMacaroonIDResponse) GetDeleted() bool { @@ -17232,7 +17507,7 @@ type MacaroonPermissionList struct { func (x *MacaroonPermissionList) Reset() { *x = MacaroonPermissionList{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[192] + mi := &file_lightning_proto_msgTypes[197] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17245,7 +17520,7 @@ func (x *MacaroonPermissionList) String() string { func (*MacaroonPermissionList) ProtoMessage() {} func (x *MacaroonPermissionList) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[192] + mi := &file_lightning_proto_msgTypes[197] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17258,7 +17533,7 @@ func (x *MacaroonPermissionList) ProtoReflect() protoreflect.Message { // Deprecated: Use MacaroonPermissionList.ProtoReflect.Descriptor instead. func (*MacaroonPermissionList) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{192} + return file_lightning_proto_rawDescGZIP(), []int{197} } func (x *MacaroonPermissionList) GetPermissions() []*MacaroonPermission { @@ -17277,7 +17552,7 @@ type ListPermissionsRequest struct { func (x *ListPermissionsRequest) Reset() { *x = ListPermissionsRequest{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[193] + mi := &file_lightning_proto_msgTypes[198] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17290,7 +17565,7 @@ func (x *ListPermissionsRequest) String() string { func (*ListPermissionsRequest) ProtoMessage() {} func (x *ListPermissionsRequest) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[193] + mi := &file_lightning_proto_msgTypes[198] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17303,7 +17578,7 @@ func (x *ListPermissionsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListPermissionsRequest.ProtoReflect.Descriptor instead. func (*ListPermissionsRequest) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{193} + return file_lightning_proto_rawDescGZIP(), []int{198} } type ListPermissionsResponse struct { @@ -17319,7 +17594,7 @@ type ListPermissionsResponse struct { func (x *ListPermissionsResponse) Reset() { *x = ListPermissionsResponse{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[194] + mi := &file_lightning_proto_msgTypes[199] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17332,7 +17607,7 @@ func (x *ListPermissionsResponse) String() string { func (*ListPermissionsResponse) ProtoMessage() {} func (x *ListPermissionsResponse) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[194] + mi := &file_lightning_proto_msgTypes[199] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17345,7 +17620,7 @@ func (x *ListPermissionsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListPermissionsResponse.ProtoReflect.Descriptor instead. func (*ListPermissionsResponse) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{194} + return file_lightning_proto_rawDescGZIP(), []int{199} } func (x *ListPermissionsResponse) GetMethodPermissions() map[string]*MacaroonPermissionList { @@ -17382,7 +17657,7 @@ type Failure struct { func (x *Failure) Reset() { *x = Failure{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[195] + mi := &file_lightning_proto_msgTypes[200] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17395,7 +17670,7 @@ func (x *Failure) String() string { func (*Failure) ProtoMessage() {} func (x *Failure) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[195] + mi := &file_lightning_proto_msgTypes[200] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17408,7 +17683,7 @@ func (x *Failure) ProtoReflect() protoreflect.Message { // Deprecated: Use Failure.ProtoReflect.Descriptor instead. func (*Failure) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{195} + return file_lightning_proto_rawDescGZIP(), []int{200} } func (x *Failure) GetCode() Failure_FailureCode { @@ -17522,7 +17797,7 @@ type ChannelUpdate struct { func (x *ChannelUpdate) Reset() { *x = ChannelUpdate{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[196] + mi := &file_lightning_proto_msgTypes[201] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17535,7 +17810,7 @@ func (x *ChannelUpdate) String() string { func (*ChannelUpdate) ProtoMessage() {} func (x *ChannelUpdate) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[196] + mi := &file_lightning_proto_msgTypes[201] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17548,7 +17823,7 @@ func (x *ChannelUpdate) ProtoReflect() protoreflect.Message { // Deprecated: Use ChannelUpdate.ProtoReflect.Descriptor instead. func (*ChannelUpdate) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{196} + return file_lightning_proto_rawDescGZIP(), []int{201} } func (x *ChannelUpdate) GetSignature() []byte { @@ -17648,7 +17923,7 @@ type MacaroonId struct { func (x *MacaroonId) Reset() { *x = MacaroonId{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[197] + mi := &file_lightning_proto_msgTypes[202] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17661,7 +17936,7 @@ func (x *MacaroonId) String() string { func (*MacaroonId) ProtoMessage() {} func (x *MacaroonId) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[197] + mi := &file_lightning_proto_msgTypes[202] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17674,7 +17949,7 @@ func (x *MacaroonId) ProtoReflect() protoreflect.Message { // Deprecated: Use MacaroonId.ProtoReflect.Descriptor instead. func (*MacaroonId) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{197} + return file_lightning_proto_rawDescGZIP(), []int{202} } func (x *MacaroonId) GetNonce() []byte { @@ -17710,7 +17985,7 @@ type Op struct { func (x *Op) Reset() { *x = Op{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[198] + mi := &file_lightning_proto_msgTypes[203] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17723,7 +17998,7 @@ func (x *Op) String() string { func (*Op) ProtoMessage() {} func (x *Op) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[198] + mi := &file_lightning_proto_msgTypes[203] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17736,7 +18011,7 @@ func (x *Op) ProtoReflect() protoreflect.Message { // Deprecated: Use Op.ProtoReflect.Descriptor instead. func (*Op) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{198} + return file_lightning_proto_rawDescGZIP(), []int{203} } func (x *Op) GetEntity() string { @@ -17791,7 +18066,7 @@ type CheckMacPermRequest struct { func (x *CheckMacPermRequest) Reset() { *x = CheckMacPermRequest{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[199] + mi := &file_lightning_proto_msgTypes[204] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17804,7 +18079,7 @@ func (x *CheckMacPermRequest) String() string { func (*CheckMacPermRequest) ProtoMessage() {} func (x *CheckMacPermRequest) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[199] + mi := &file_lightning_proto_msgTypes[204] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17817,7 +18092,7 @@ func (x *CheckMacPermRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckMacPermRequest.ProtoReflect.Descriptor instead. func (*CheckMacPermRequest) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{199} + return file_lightning_proto_rawDescGZIP(), []int{204} } func (x *CheckMacPermRequest) GetMacaroon() []byte { @@ -17859,7 +18134,7 @@ type CheckMacPermResponse struct { func (x *CheckMacPermResponse) Reset() { *x = CheckMacPermResponse{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[200] + mi := &file_lightning_proto_msgTypes[205] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17872,7 +18147,7 @@ func (x *CheckMacPermResponse) String() string { func (*CheckMacPermResponse) ProtoMessage() {} func (x *CheckMacPermResponse) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[200] + mi := &file_lightning_proto_msgTypes[205] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17885,7 +18160,7 @@ func (x *CheckMacPermResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckMacPermResponse.ProtoReflect.Descriptor instead. func (*CheckMacPermResponse) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{200} + return file_lightning_proto_rawDescGZIP(), []int{205} } func (x *CheckMacPermResponse) GetValid() bool { @@ -17945,7 +18220,7 @@ type RPCMiddlewareRequest struct { func (x *RPCMiddlewareRequest) Reset() { *x = RPCMiddlewareRequest{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[201] + mi := &file_lightning_proto_msgTypes[206] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17958,7 +18233,7 @@ func (x *RPCMiddlewareRequest) String() string { func (*RPCMiddlewareRequest) ProtoMessage() {} func (x *RPCMiddlewareRequest) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[201] + mi := &file_lightning_proto_msgTypes[206] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17971,7 +18246,7 @@ func (x *RPCMiddlewareRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RPCMiddlewareRequest.ProtoReflect.Descriptor instead. func (*RPCMiddlewareRequest) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{201} + return file_lightning_proto_rawDescGZIP(), []int{206} } func (x *RPCMiddlewareRequest) GetRequestId() uint64 { @@ -18104,7 +18379,7 @@ type MetadataValues struct { func (x *MetadataValues) Reset() { *x = MetadataValues{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[202] + mi := &file_lightning_proto_msgTypes[207] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18117,7 +18392,7 @@ func (x *MetadataValues) String() string { func (*MetadataValues) ProtoMessage() {} func (x *MetadataValues) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[202] + mi := &file_lightning_proto_msgTypes[207] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18130,7 +18405,7 @@ func (x *MetadataValues) ProtoReflect() protoreflect.Message { // Deprecated: Use MetadataValues.ProtoReflect.Descriptor instead. func (*MetadataValues) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{202} + return file_lightning_proto_rawDescGZIP(), []int{207} } func (x *MetadataValues) GetValues() []string { @@ -18154,7 +18429,7 @@ type StreamAuth struct { func (x *StreamAuth) Reset() { *x = StreamAuth{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[203] + mi := &file_lightning_proto_msgTypes[208] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18167,7 +18442,7 @@ func (x *StreamAuth) String() string { func (*StreamAuth) ProtoMessage() {} func (x *StreamAuth) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[203] + mi := &file_lightning_proto_msgTypes[208] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18180,7 +18455,7 @@ func (x *StreamAuth) ProtoReflect() protoreflect.Message { // Deprecated: Use StreamAuth.ProtoReflect.Descriptor instead. func (*StreamAuth) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{203} + return file_lightning_proto_rawDescGZIP(), []int{208} } func (x *StreamAuth) GetMethodFullUri() string { @@ -18217,7 +18492,7 @@ type RPCMessage struct { func (x *RPCMessage) Reset() { *x = RPCMessage{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[204] + mi := &file_lightning_proto_msgTypes[209] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18230,7 +18505,7 @@ func (x *RPCMessage) String() string { func (*RPCMessage) ProtoMessage() {} func (x *RPCMessage) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[204] + mi := &file_lightning_proto_msgTypes[209] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18243,7 +18518,7 @@ func (x *RPCMessage) ProtoReflect() protoreflect.Message { // Deprecated: Use RPCMessage.ProtoReflect.Descriptor instead. func (*RPCMessage) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{204} + return file_lightning_proto_rawDescGZIP(), []int{209} } func (x *RPCMessage) GetMethodFullUri() string { @@ -18304,7 +18579,7 @@ type RPCMiddlewareResponse struct { func (x *RPCMiddlewareResponse) Reset() { *x = RPCMiddlewareResponse{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[205] + mi := &file_lightning_proto_msgTypes[210] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18317,7 +18592,7 @@ func (x *RPCMiddlewareResponse) String() string { func (*RPCMiddlewareResponse) ProtoMessage() {} func (x *RPCMiddlewareResponse) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[205] + mi := &file_lightning_proto_msgTypes[210] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18330,7 +18605,7 @@ func (x *RPCMiddlewareResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RPCMiddlewareResponse.ProtoReflect.Descriptor instead. func (*RPCMiddlewareResponse) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{205} + return file_lightning_proto_rawDescGZIP(), []int{210} } func (x *RPCMiddlewareResponse) GetRefMsgId() uint64 { @@ -18416,7 +18691,7 @@ type MiddlewareRegistration struct { func (x *MiddlewareRegistration) Reset() { *x = MiddlewareRegistration{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[206] + mi := &file_lightning_proto_msgTypes[211] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18429,7 +18704,7 @@ func (x *MiddlewareRegistration) String() string { func (*MiddlewareRegistration) ProtoMessage() {} func (x *MiddlewareRegistration) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[206] + mi := &file_lightning_proto_msgTypes[211] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18442,7 +18717,7 @@ func (x *MiddlewareRegistration) ProtoReflect() protoreflect.Message { // Deprecated: Use MiddlewareRegistration.ProtoReflect.Descriptor instead. func (*MiddlewareRegistration) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{206} + return file_lightning_proto_rawDescGZIP(), []int{211} } func (x *MiddlewareRegistration) GetMiddlewareName() string { @@ -18489,7 +18764,7 @@ type InterceptFeedback struct { func (x *InterceptFeedback) Reset() { *x = InterceptFeedback{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[207] + mi := &file_lightning_proto_msgTypes[212] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18502,7 +18777,7 @@ func (x *InterceptFeedback) String() string { func (*InterceptFeedback) ProtoMessage() {} func (x *InterceptFeedback) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[207] + mi := &file_lightning_proto_msgTypes[212] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18515,7 +18790,7 @@ func (x *InterceptFeedback) ProtoReflect() protoreflect.Message { // Deprecated: Use InterceptFeedback.ProtoReflect.Descriptor instead. func (*InterceptFeedback) Descriptor() ([]byte, []int) { - return file_lightning_proto_rawDescGZIP(), []int{207} + return file_lightning_proto_rawDescGZIP(), []int{212} } func (x *InterceptFeedback) GetError() string { @@ -18576,7 +18851,7 @@ type PendingChannelsResponse_PendingChannel struct { func (x *PendingChannelsResponse_PendingChannel) Reset() { *x = PendingChannelsResponse_PendingChannel{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[214] + mi := &file_lightning_proto_msgTypes[219] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18589,7 +18864,7 @@ func (x *PendingChannelsResponse_PendingChannel) String() string { func (*PendingChannelsResponse_PendingChannel) ProtoMessage() {} func (x *PendingChannelsResponse_PendingChannel) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[214] + mi := &file_lightning_proto_msgTypes[219] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18754,7 +19029,7 @@ type PendingChannelsResponse_PendingOpenChannel struct { func (x *PendingChannelsResponse_PendingOpenChannel) Reset() { *x = PendingChannelsResponse_PendingOpenChannel{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[215] + mi := &file_lightning_proto_msgTypes[220] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18767,7 +19042,7 @@ func (x *PendingChannelsResponse_PendingOpenChannel) String() string { func (*PendingChannelsResponse_PendingOpenChannel) ProtoMessage() {} func (x *PendingChannelsResponse_PendingOpenChannel) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[215] + mi := &file_lightning_proto_msgTypes[220] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18854,7 +19129,7 @@ type PendingChannelsResponse_WaitingCloseChannel struct { func (x *PendingChannelsResponse_WaitingCloseChannel) Reset() { *x = PendingChannelsResponse_WaitingCloseChannel{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[216] + mi := &file_lightning_proto_msgTypes[221] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18867,7 +19142,7 @@ func (x *PendingChannelsResponse_WaitingCloseChannel) String() string { func (*PendingChannelsResponse_WaitingCloseChannel) ProtoMessage() {} func (x *PendingChannelsResponse_WaitingCloseChannel) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[216] + mi := &file_lightning_proto_msgTypes[221] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -18943,7 +19218,7 @@ type PendingChannelsResponse_Commitments struct { func (x *PendingChannelsResponse_Commitments) Reset() { *x = PendingChannelsResponse_Commitments{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[217] + mi := &file_lightning_proto_msgTypes[222] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -18956,7 +19231,7 @@ func (x *PendingChannelsResponse_Commitments) String() string { func (*PendingChannelsResponse_Commitments) ProtoMessage() {} func (x *PendingChannelsResponse_Commitments) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[217] + mi := &file_lightning_proto_msgTypes[222] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -19028,7 +19303,7 @@ type PendingChannelsResponse_ClosedChannel struct { func (x *PendingChannelsResponse_ClosedChannel) Reset() { *x = PendingChannelsResponse_ClosedChannel{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[218] + mi := &file_lightning_proto_msgTypes[223] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -19041,7 +19316,7 @@ func (x *PendingChannelsResponse_ClosedChannel) String() string { func (*PendingChannelsResponse_ClosedChannel) ProtoMessage() {} func (x *PendingChannelsResponse_ClosedChannel) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[218] + mi := &file_lightning_proto_msgTypes[223] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -19097,7 +19372,7 @@ type PendingChannelsResponse_ForceClosedChannel struct { func (x *PendingChannelsResponse_ForceClosedChannel) Reset() { *x = PendingChannelsResponse_ForceClosedChannel{} if protoimpl.UnsafeEnabled { - mi := &file_lightning_proto_msgTypes[219] + mi := &file_lightning_proto_msgTypes[224] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -19110,7 +19385,7 @@ func (x *PendingChannelsResponse_ForceClosedChannel) String() string { func (*PendingChannelsResponse_ForceClosedChannel) ProtoMessage() {} func (x *PendingChannelsResponse_ForceClosedChannel) ProtoReflect() protoreflect.Message { - mi := &file_lightning_proto_msgTypes[219] + mi := &file_lightning_proto_msgTypes[224] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -21445,1031 +21720,1079 @@ var file_lightning_proto_rawDesc = []byte{ 0x65, 0x78, 0x4f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x2c, 0x0a, 0x12, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x6e, 0x75, 0x6d, 0x5f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x10, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x4e, 0x75, 0x6d, 0x50, 0x61, - 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x65, 0x0a, 0x14, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, - 0x0a, 0x0c, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x48, 0x61, 0x73, - 0x68, 0x12, 0x2a, 0x0a, 0x11, 0x66, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x5f, 0x68, 0x74, 0x6c, 0x63, - 0x73, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x66, 0x61, - 0x69, 0x6c, 0x65, 0x64, 0x48, 0x74, 0x6c, 0x63, 0x73, 0x4f, 0x6e, 0x6c, 0x79, 0x22, 0x9b, 0x01, - 0x0a, 0x18, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x6c, 0x6c, 0x50, 0x61, 0x79, 0x6d, 0x65, - 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x30, 0x0a, 0x14, 0x66, 0x61, - 0x69, 0x6c, 0x65, 0x64, 0x5f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x5f, 0x6f, 0x6e, - 0x6c, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x66, 0x61, 0x69, 0x6c, 0x65, 0x64, - 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x4f, 0x6e, 0x6c, 0x79, 0x12, 0x2a, 0x0a, 0x11, - 0x66, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x5f, 0x68, 0x74, 0x6c, 0x63, 0x73, 0x5f, 0x6f, 0x6e, 0x6c, - 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x66, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x48, - 0x74, 0x6c, 0x63, 0x73, 0x4f, 0x6e, 0x6c, 0x79, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x6c, 0x6c, 0x5f, - 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, - 0x61, 0x6c, 0x6c, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x2f, 0x0a, 0x15, 0x44, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x33, 0x0a, 0x19, - 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x6c, 0x6c, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, - 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x22, 0xbf, 0x01, 0x0a, 0x15, 0x41, 0x62, 0x61, 0x6e, 0x64, 0x6f, 0x6e, 0x43, 0x68, 0x61, - 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x38, 0x0a, 0x0d, 0x63, - 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, - 0x65, 0x6c, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x0c, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, - 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x39, 0x0a, 0x19, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, - 0x5f, 0x66, 0x75, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x73, 0x68, 0x69, 0x6d, 0x5f, 0x6f, 0x6e, - 0x6c, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x16, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, - 0x67, 0x46, 0x75, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x53, 0x68, 0x69, 0x6d, 0x4f, 0x6e, 0x6c, 0x79, - 0x12, 0x31, 0x0a, 0x16, 0x69, 0x5f, 0x6b, 0x6e, 0x6f, 0x77, 0x5f, 0x77, 0x68, 0x61, 0x74, 0x5f, - 0x69, 0x5f, 0x61, 0x6d, 0x5f, 0x64, 0x6f, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x11, 0x69, 0x4b, 0x6e, 0x6f, 0x77, 0x57, 0x68, 0x61, 0x74, 0x49, 0x41, 0x6d, 0x44, 0x6f, - 0x69, 0x6e, 0x67, 0x22, 0x30, 0x0a, 0x16, 0x41, 0x62, 0x61, 0x6e, 0x64, 0x6f, 0x6e, 0x43, 0x68, - 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, - 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x46, 0x0a, 0x11, 0x44, 0x65, 0x62, 0x75, 0x67, 0x4c, 0x65, - 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x68, - 0x6f, 0x77, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x73, 0x68, 0x6f, 0x77, 0x12, 0x1d, - 0x0a, 0x0a, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x09, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x53, 0x70, 0x65, 0x63, 0x22, 0x35, 0x0a, - 0x12, 0x44, 0x65, 0x62, 0x75, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x75, 0x62, 0x5f, 0x73, 0x79, 0x73, 0x74, 0x65, - 0x6d, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x75, 0x62, 0x53, 0x79, 0x73, - 0x74, 0x65, 0x6d, 0x73, 0x22, 0x27, 0x0a, 0x0c, 0x50, 0x61, 0x79, 0x52, 0x65, 0x71, 0x53, 0x74, - 0x72, 0x69, 0x6e, 0x67, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x5f, 0x72, 0x65, 0x71, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x61, 0x79, 0x52, 0x65, 0x71, 0x22, 0xf0, 0x04, - 0x0a, 0x06, 0x50, 0x61, 0x79, 0x52, 0x65, 0x71, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x74, - 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, - 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x61, - 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0b, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x48, 0x61, 0x73, 0x68, 0x12, 0x21, 0x0a, - 0x0c, 0x6e, 0x75, 0x6d, 0x5f, 0x73, 0x61, 0x74, 0x6f, 0x73, 0x68, 0x69, 0x73, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x0b, 0x6e, 0x75, 0x6d, 0x53, 0x61, 0x74, 0x6f, 0x73, 0x68, 0x69, 0x73, - 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x16, - 0x0a, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, - 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, - 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x29, 0x0a, 0x10, 0x64, 0x65, 0x73, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x07, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x48, - 0x61, 0x73, 0x68, 0x12, 0x23, 0x0a, 0x0d, 0x66, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x5f, - 0x61, 0x64, 0x64, 0x72, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x66, 0x61, 0x6c, 0x6c, - 0x62, 0x61, 0x63, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6c, 0x74, 0x76, - 0x5f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x63, - 0x6c, 0x74, 0x76, 0x45, 0x78, 0x70, 0x69, 0x72, 0x79, 0x12, 0x31, 0x0a, 0x0b, 0x72, 0x6f, 0x75, - 0x74, 0x65, 0x5f, 0x68, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, - 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x48, 0x69, 0x6e, 0x74, - 0x52, 0x0a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x48, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x21, 0x0a, 0x0c, - 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x0b, 0x20, 0x01, - 0x28, 0x0c, 0x52, 0x0b, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x41, 0x64, 0x64, 0x72, 0x12, - 0x19, 0x0a, 0x08, 0x6e, 0x75, 0x6d, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x07, 0x6e, 0x75, 0x6d, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x37, 0x0a, 0x08, 0x66, 0x65, - 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6c, - 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x61, 0x79, 0x52, 0x65, 0x71, 0x2e, 0x46, 0x65, 0x61, 0x74, - 0x75, 0x72, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, - 0x72, 0x65, 0x73, 0x12, 0x3e, 0x0a, 0x0d, 0x62, 0x6c, 0x69, 0x6e, 0x64, 0x65, 0x64, 0x5f, 0x70, - 0x61, 0x74, 0x68, 0x73, 0x18, 0x0e, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6c, 0x6e, 0x72, - 0x70, 0x63, 0x2e, 0x42, 0x6c, 0x69, 0x6e, 0x64, 0x65, 0x64, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, - 0x74, 0x50, 0x61, 0x74, 0x68, 0x52, 0x0c, 0x62, 0x6c, 0x69, 0x6e, 0x64, 0x65, 0x64, 0x50, 0x61, - 0x74, 0x68, 0x73, 0x1a, 0x4b, 0x0a, 0x0d, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x45, - 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0d, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x65, - 0x61, 0x74, 0x75, 0x72, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, - 0x22, 0x59, 0x0a, 0x07, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, - 0x1f, 0x0a, 0x0b, 0x69, 0x73, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x69, 0x73, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, - 0x12, 0x19, 0x0a, 0x08, 0x69, 0x73, 0x5f, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x07, 0x69, 0x73, 0x4b, 0x6e, 0x6f, 0x77, 0x6e, 0x22, 0x12, 0x0a, 0x10, 0x46, - 0x65, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, - 0x95, 0x02, 0x0a, 0x10, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x46, 0x65, 0x65, 0x52, 0x65, - 0x70, 0x6f, 0x72, 0x74, 0x12, 0x1b, 0x0a, 0x07, 0x63, 0x68, 0x61, 0x6e, 0x5f, 0x69, 0x64, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, 0x30, 0x01, 0x52, 0x06, 0x63, 0x68, 0x61, 0x6e, 0x49, - 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x70, 0x6f, 0x69, - 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, - 0x6c, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x22, 0x0a, 0x0d, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x66, - 0x65, 0x65, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x62, - 0x61, 0x73, 0x65, 0x46, 0x65, 0x65, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x1e, 0x0a, 0x0b, 0x66, 0x65, - 0x65, 0x5f, 0x70, 0x65, 0x72, 0x5f, 0x6d, 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x09, 0x66, 0x65, 0x65, 0x50, 0x65, 0x72, 0x4d, 0x69, 0x6c, 0x12, 0x19, 0x0a, 0x08, 0x66, 0x65, - 0x65, 0x5f, 0x72, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x01, 0x52, 0x07, 0x66, 0x65, - 0x65, 0x52, 0x61, 0x74, 0x65, 0x12, 0x31, 0x0a, 0x15, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, - 0x5f, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x05, 0x52, 0x12, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x42, 0x61, 0x73, - 0x65, 0x46, 0x65, 0x65, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x2d, 0x0a, 0x13, 0x69, 0x6e, 0x62, 0x6f, - 0x75, 0x6e, 0x64, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x70, 0x65, 0x72, 0x5f, 0x6d, 0x69, 0x6c, 0x18, - 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, 0x10, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x46, 0x65, - 0x65, 0x50, 0x65, 0x72, 0x4d, 0x69, 0x6c, 0x22, 0xb5, 0x01, 0x0a, 0x11, 0x46, 0x65, 0x65, 0x52, - 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a, 0x0a, - 0x0c, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x66, 0x65, 0x65, 0x73, 0x18, 0x01, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, - 0x6e, 0x65, 0x6c, 0x46, 0x65, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x0b, 0x63, 0x68, - 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x46, 0x65, 0x65, 0x73, 0x12, 0x1e, 0x0a, 0x0b, 0x64, 0x61, 0x79, - 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x73, 0x75, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, - 0x64, 0x61, 0x79, 0x46, 0x65, 0x65, 0x53, 0x75, 0x6d, 0x12, 0x20, 0x0a, 0x0c, 0x77, 0x65, 0x65, - 0x6b, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x73, 0x75, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, - 0x0a, 0x77, 0x65, 0x65, 0x6b, 0x46, 0x65, 0x65, 0x53, 0x75, 0x6d, 0x12, 0x22, 0x0a, 0x0d, 0x6d, - 0x6f, 0x6e, 0x74, 0x68, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x73, 0x75, 0x6d, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x04, 0x52, 0x0b, 0x6d, 0x6f, 0x6e, 0x74, 0x68, 0x46, 0x65, 0x65, 0x53, 0x75, 0x6d, 0x22, - 0x52, 0x0a, 0x0a, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x46, 0x65, 0x65, 0x12, 0x22, 0x0a, - 0x0d, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x62, 0x61, 0x73, 0x65, 0x46, 0x65, 0x65, 0x4d, 0x73, 0x61, - 0x74, 0x12, 0x20, 0x0a, 0x0c, 0x66, 0x65, 0x65, 0x5f, 0x72, 0x61, 0x74, 0x65, 0x5f, 0x70, 0x70, - 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x66, 0x65, 0x65, 0x52, 0x61, 0x74, 0x65, - 0x50, 0x70, 0x6d, 0x22, 0xda, 0x03, 0x0a, 0x13, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x06, 0x67, - 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x06, 0x67, - 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x12, 0x34, 0x0a, 0x0a, 0x63, 0x68, 0x61, 0x6e, 0x5f, 0x70, 0x6f, - 0x69, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x6c, 0x6e, 0x72, 0x70, - 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x48, 0x00, - 0x52, 0x09, 0x63, 0x68, 0x61, 0x6e, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x22, 0x0a, 0x0d, 0x62, - 0x61, 0x73, 0x65, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x0b, 0x62, 0x61, 0x73, 0x65, 0x46, 0x65, 0x65, 0x4d, 0x73, 0x61, 0x74, 0x12, - 0x19, 0x0a, 0x08, 0x66, 0x65, 0x65, 0x5f, 0x72, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x01, 0x52, 0x07, 0x66, 0x65, 0x65, 0x52, 0x61, 0x74, 0x65, 0x12, 0x20, 0x0a, 0x0c, 0x66, 0x65, - 0x65, 0x5f, 0x72, 0x61, 0x74, 0x65, 0x5f, 0x70, 0x70, 0x6d, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0d, - 0x52, 0x0a, 0x66, 0x65, 0x65, 0x52, 0x61, 0x74, 0x65, 0x50, 0x70, 0x6d, 0x12, 0x26, 0x0a, 0x0f, - 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0d, 0x74, 0x69, 0x6d, 0x65, 0x4c, 0x6f, 0x63, 0x6b, 0x44, - 0x65, 0x6c, 0x74, 0x61, 0x12, 0x22, 0x0a, 0x0d, 0x6d, 0x61, 0x78, 0x5f, 0x68, 0x74, 0x6c, 0x63, - 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x6d, 0x61, 0x78, - 0x48, 0x74, 0x6c, 0x63, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x22, 0x0a, 0x0d, 0x6d, 0x69, 0x6e, 0x5f, - 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x52, - 0x0b, 0x6d, 0x69, 0x6e, 0x48, 0x74, 0x6c, 0x63, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x35, 0x0a, 0x17, - 0x6d, 0x69, 0x6e, 0x5f, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x5f, 0x73, 0x70, - 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x6d, - 0x69, 0x6e, 0x48, 0x74, 0x6c, 0x63, 0x4d, 0x73, 0x61, 0x74, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, - 0x69, 0x65, 0x64, 0x12, 0x32, 0x0a, 0x0b, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x66, - 0x65, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, - 0x2e, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x46, 0x65, 0x65, 0x52, 0x0a, 0x69, 0x6e, 0x62, - 0x6f, 0x75, 0x6e, 0x64, 0x46, 0x65, 0x65, 0x12, 0x2e, 0x0a, 0x13, 0x63, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x5f, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x5f, 0x65, 0x64, 0x67, 0x65, 0x18, 0x0b, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x69, 0x73, 0x73, - 0x69, 0x6e, 0x67, 0x45, 0x64, 0x67, 0x65, 0x42, 0x07, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, - 0x22, 0x8c, 0x01, 0x0a, 0x0c, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x12, 0x2b, 0x0a, 0x08, 0x6f, 0x75, 0x74, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4f, 0x75, 0x74, 0x50, - 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x08, 0x6f, 0x75, 0x74, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x2c, - 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x14, - 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x46, 0x61, 0x69, - 0x6c, 0x75, 0x72, 0x65, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x21, 0x0a, 0x0c, - 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x22, - 0x52, 0x0a, 0x14, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a, 0x0a, 0x0e, 0x66, 0x61, 0x69, 0x6c, 0x65, - 0x64, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x13, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x52, 0x0d, 0x66, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x73, 0x22, 0xa1, 0x02, 0x0a, 0x18, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, - 0x6e, 0x67, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, - 0x19, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x04, 0x52, 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x69, 0x6e, - 0x64, 0x65, 0x78, 0x5f, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, - 0x52, 0x0b, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x4f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x24, 0x0a, - 0x0e, 0x6e, 0x75, 0x6d, 0x5f, 0x6d, 0x61, 0x78, 0x5f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0c, 0x6e, 0x75, 0x6d, 0x4d, 0x61, 0x78, 0x45, 0x76, 0x65, - 0x6e, 0x74, 0x73, 0x12, 0x2a, 0x0a, 0x11, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x61, 0x6c, 0x69, 0x61, - 0x73, 0x5f, 0x6c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, - 0x70, 0x65, 0x65, 0x72, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x12, - 0x2a, 0x0a, 0x11, 0x69, 0x6e, 0x63, 0x6f, 0x6d, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x68, 0x61, 0x6e, - 0x5f, 0x69, 0x64, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x04, 0x52, 0x0f, 0x69, 0x6e, 0x63, 0x6f, - 0x6d, 0x69, 0x6e, 0x67, 0x43, 0x68, 0x61, 0x6e, 0x49, 0x64, 0x73, 0x12, 0x2a, 0x0a, 0x11, 0x6f, - 0x75, 0x74, 0x67, 0x6f, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x5f, 0x69, 0x64, 0x73, - 0x18, 0x07, 0x20, 0x03, 0x28, 0x04, 0x52, 0x0f, 0x6f, 0x75, 0x74, 0x67, 0x6f, 0x69, 0x6e, 0x67, - 0x43, 0x68, 0x61, 0x6e, 0x49, 0x64, 0x73, 0x22, 0x8d, 0x04, 0x0a, 0x0f, 0x46, 0x6f, 0x72, 0x77, - 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x20, 0x0a, 0x09, 0x74, - 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, - 0x18, 0x01, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x20, 0x0a, - 0x0a, 0x63, 0x68, 0x61, 0x6e, 0x5f, 0x69, 0x64, 0x5f, 0x69, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x04, 0x42, 0x02, 0x30, 0x01, 0x52, 0x08, 0x63, 0x68, 0x61, 0x6e, 0x49, 0x64, 0x49, 0x6e, 0x12, - 0x22, 0x0a, 0x0b, 0x63, 0x68, 0x61, 0x6e, 0x5f, 0x69, 0x64, 0x5f, 0x6f, 0x75, 0x74, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, 0x30, 0x01, 0x52, 0x09, 0x63, 0x68, 0x61, 0x6e, 0x49, 0x64, - 0x4f, 0x75, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x61, 0x6d, 0x74, 0x5f, 0x69, 0x6e, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x04, 0x52, 0x05, 0x61, 0x6d, 0x74, 0x49, 0x6e, 0x12, 0x17, 0x0a, 0x07, 0x61, 0x6d, - 0x74, 0x5f, 0x6f, 0x75, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x61, 0x6d, 0x74, - 0x4f, 0x75, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x66, 0x65, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, - 0x52, 0x03, 0x66, 0x65, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x66, 0x65, 0x65, 0x5f, 0x6d, 0x73, 0x61, - 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x66, 0x65, 0x65, 0x4d, 0x73, 0x61, 0x74, - 0x12, 0x1e, 0x0a, 0x0b, 0x61, 0x6d, 0x74, 0x5f, 0x69, 0x6e, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, - 0x09, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x61, 0x6d, 0x74, 0x49, 0x6e, 0x4d, 0x73, 0x61, 0x74, - 0x12, 0x20, 0x0a, 0x0c, 0x61, 0x6d, 0x74, 0x5f, 0x6f, 0x75, 0x74, 0x5f, 0x6d, 0x73, 0x61, 0x74, - 0x18, 0x0a, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x61, 0x6d, 0x74, 0x4f, 0x75, 0x74, 0x4d, 0x73, - 0x61, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x5f, - 0x6e, 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x4e, 0x73, 0x12, 0x22, 0x0a, 0x0d, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x61, 0x6c, - 0x69, 0x61, 0x73, 0x5f, 0x69, 0x6e, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x65, - 0x65, 0x72, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x49, 0x6e, 0x12, 0x24, 0x0a, 0x0e, 0x70, 0x65, 0x65, - 0x72, 0x5f, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x5f, 0x6f, 0x75, 0x74, 0x18, 0x0d, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0c, 0x70, 0x65, 0x65, 0x72, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x4f, 0x75, 0x74, 0x12, - 0x2d, 0x0a, 0x10, 0x69, 0x6e, 0x63, 0x6f, 0x6d, 0x69, 0x6e, 0x67, 0x5f, 0x68, 0x74, 0x6c, 0x63, - 0x5f, 0x69, 0x64, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x04, 0x48, 0x00, 0x52, 0x0e, 0x69, 0x6e, 0x63, - 0x6f, 0x6d, 0x69, 0x6e, 0x67, 0x48, 0x74, 0x6c, 0x63, 0x49, 0x64, 0x88, 0x01, 0x01, 0x12, 0x2d, - 0x0a, 0x10, 0x6f, 0x75, 0x74, 0x67, 0x6f, 0x69, 0x6e, 0x67, 0x5f, 0x68, 0x74, 0x6c, 0x63, 0x5f, - 0x69, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x04, 0x48, 0x01, 0x52, 0x0e, 0x6f, 0x75, 0x74, 0x67, - 0x6f, 0x69, 0x6e, 0x67, 0x48, 0x74, 0x6c, 0x63, 0x49, 0x64, 0x88, 0x01, 0x01, 0x42, 0x13, 0x0a, - 0x11, 0x5f, 0x69, 0x6e, 0x63, 0x6f, 0x6d, 0x69, 0x6e, 0x67, 0x5f, 0x68, 0x74, 0x6c, 0x63, 0x5f, - 0x69, 0x64, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x6f, 0x75, 0x74, 0x67, 0x6f, 0x69, 0x6e, 0x67, 0x5f, - 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x69, 0x64, 0x22, 0x8c, 0x01, 0x0a, 0x19, 0x46, 0x6f, 0x72, 0x77, - 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x11, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, - 0x69, 0x6e, 0x67, 0x5f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x16, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, - 0x69, 0x6e, 0x67, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x10, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, - 0x64, 0x69, 0x6e, 0x67, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x2a, 0x0a, 0x11, 0x6c, 0x61, - 0x73, 0x74, 0x5f, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0f, 0x6c, 0x61, 0x73, 0x74, 0x4f, 0x66, 0x66, 0x73, 0x65, - 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x22, 0x50, 0x0a, 0x1a, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, - 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x32, 0x0a, 0x0a, 0x63, 0x68, 0x61, 0x6e, 0x5f, 0x70, 0x6f, 0x69, - 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, - 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x09, 0x63, - 0x68, 0x61, 0x6e, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x22, 0x64, 0x0a, 0x0d, 0x43, 0x68, 0x61, 0x6e, - 0x6e, 0x65, 0x6c, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x12, 0x32, 0x0a, 0x0a, 0x63, 0x68, 0x61, - 0x6e, 0x5f, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, - 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x50, 0x6f, 0x69, - 0x6e, 0x74, 0x52, 0x09, 0x63, 0x68, 0x61, 0x6e, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x1f, 0x0a, - 0x0b, 0x63, 0x68, 0x61, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0c, 0x52, 0x0a, 0x63, 0x68, 0x61, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x22, 0x73, - 0x0a, 0x0f, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x43, 0x68, 0x61, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x75, - 0x70, 0x12, 0x34, 0x0a, 0x0b, 0x63, 0x68, 0x61, 0x6e, 0x5f, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, - 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x0a, 0x63, 0x68, 0x61, - 0x6e, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x2a, 0x0a, 0x11, 0x6d, 0x75, 0x6c, 0x74, 0x69, + 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x41, 0x0a, 0x1c, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x61, + 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x44, 0x75, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, + 0x74, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x70, 0x61, + 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x48, 0x61, 0x73, 0x68, 0x22, 0x58, 0x0a, 0x1d, 0x4c, 0x69, 0x73, + 0x74, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x44, 0x75, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, + 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x0a, 0x64, 0x75, + 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, + 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x44, 0x75, + 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x0a, 0x64, 0x75, 0x70, 0x6c, 0x69, 0x63, 0x61, + 0x74, 0x65, 0x73, 0x22, 0x21, 0x0a, 0x1f, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x6c, 0x6c, 0x50, 0x61, + 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x44, 0x75, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x5b, 0x0a, 0x20, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x6c, + 0x6c, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x44, 0x75, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, + 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x0a, 0x64, 0x75, + 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, + 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x44, 0x75, + 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x0a, 0x64, 0x75, 0x70, 0x6c, 0x69, 0x63, 0x61, + 0x74, 0x65, 0x73, 0x22, 0x93, 0x02, 0x0a, 0x10, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x44, + 0x75, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x61, 0x79, 0x6d, + 0x65, 0x6e, 0x74, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, + 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x48, 0x61, 0x73, 0x68, 0x12, 0x1d, 0x0a, 0x0a, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x09, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x28, 0x0a, 0x10, 0x63, 0x72, + 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x6e, 0x73, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, + 0x6d, 0x65, 0x4e, 0x73, 0x12, 0x42, 0x0a, 0x0e, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x5f, + 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x6c, + 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x46, 0x61, 0x69, 0x6c, + 0x75, 0x72, 0x65, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x52, 0x0d, 0x66, 0x61, 0x69, 0x6c, 0x75, + 0x72, 0x65, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x29, 0x0a, 0x10, 0x70, 0x61, 0x79, 0x6d, + 0x65, 0x6e, 0x74, 0x5f, 0x70, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x0f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x50, 0x72, 0x65, 0x69, 0x6d, + 0x61, 0x67, 0x65, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x65, 0x74, 0x74, 0x6c, 0x65, 0x5f, 0x74, 0x69, + 0x6d, 0x65, 0x5f, 0x6e, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x73, 0x65, 0x74, + 0x74, 0x6c, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x4e, 0x73, 0x22, 0x65, 0x0a, 0x14, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x68, 0x61, 0x73, + 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, + 0x48, 0x61, 0x73, 0x68, 0x12, 0x2a, 0x0a, 0x11, 0x66, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x5f, 0x68, + 0x74, 0x6c, 0x63, 0x73, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x0f, 0x66, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x48, 0x74, 0x6c, 0x63, 0x73, 0x4f, 0x6e, 0x6c, 0x79, + 0x22, 0x9b, 0x01, 0x0a, 0x18, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x6c, 0x6c, 0x50, 0x61, + 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x30, 0x0a, + 0x14, 0x66, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x5f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, + 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x66, 0x61, 0x69, + 0x6c, 0x65, 0x64, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x4f, 0x6e, 0x6c, 0x79, 0x12, + 0x2a, 0x0a, 0x11, 0x66, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x5f, 0x68, 0x74, 0x6c, 0x63, 0x73, 0x5f, + 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x66, 0x61, 0x69, 0x6c, + 0x65, 0x64, 0x48, 0x74, 0x6c, 0x63, 0x73, 0x4f, 0x6e, 0x6c, 0x79, 0x12, 0x21, 0x0a, 0x0c, 0x61, + 0x6c, 0x6c, 0x5f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x0b, 0x61, 0x6c, 0x6c, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x2f, + 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, + 0x33, 0x0a, 0x19, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x6c, 0x6c, 0x50, 0x61, 0x79, 0x6d, + 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, + 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x22, 0xbf, 0x01, 0x0a, 0x15, 0x41, 0x62, 0x61, 0x6e, 0x64, 0x6f, 0x6e, + 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x38, + 0x0a, 0x0d, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, + 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x0c, 0x63, 0x68, 0x61, 0x6e, + 0x6e, 0x65, 0x6c, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x39, 0x0a, 0x19, 0x70, 0x65, 0x6e, 0x64, + 0x69, 0x6e, 0x67, 0x5f, 0x66, 0x75, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x73, 0x68, 0x69, 0x6d, + 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x16, 0x70, 0x65, 0x6e, + 0x64, 0x69, 0x6e, 0x67, 0x46, 0x75, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x53, 0x68, 0x69, 0x6d, 0x4f, + 0x6e, 0x6c, 0x79, 0x12, 0x31, 0x0a, 0x16, 0x69, 0x5f, 0x6b, 0x6e, 0x6f, 0x77, 0x5f, 0x77, 0x68, + 0x61, 0x74, 0x5f, 0x69, 0x5f, 0x61, 0x6d, 0x5f, 0x64, 0x6f, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x11, 0x69, 0x4b, 0x6e, 0x6f, 0x77, 0x57, 0x68, 0x61, 0x74, 0x49, 0x41, + 0x6d, 0x44, 0x6f, 0x69, 0x6e, 0x67, 0x22, 0x30, 0x0a, 0x16, 0x41, 0x62, 0x61, 0x6e, 0x64, 0x6f, + 0x6e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x46, 0x0a, 0x11, 0x44, 0x65, 0x62, 0x75, + 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, + 0x04, 0x73, 0x68, 0x6f, 0x77, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x73, 0x68, 0x6f, + 0x77, 0x12, 0x1d, 0x0a, 0x0a, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x53, 0x70, 0x65, 0x63, + 0x22, 0x35, 0x0a, 0x12, 0x44, 0x65, 0x62, 0x75, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x75, 0x62, 0x5f, 0x73, 0x79, + 0x73, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x75, 0x62, + 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x73, 0x22, 0x27, 0x0a, 0x0c, 0x50, 0x61, 0x79, 0x52, 0x65, + 0x71, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x5f, 0x72, + 0x65, 0x71, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x61, 0x79, 0x52, 0x65, 0x71, + 0x22, 0xf0, 0x04, 0x0a, 0x06, 0x50, 0x61, 0x79, 0x52, 0x65, 0x71, 0x12, 0x20, 0x0a, 0x0b, 0x64, + 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x0a, + 0x0c, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x48, 0x61, 0x73, 0x68, + 0x12, 0x21, 0x0a, 0x0c, 0x6e, 0x75, 0x6d, 0x5f, 0x73, 0x61, 0x74, 0x6f, 0x73, 0x68, 0x69, 0x73, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x6e, 0x75, 0x6d, 0x53, 0x61, 0x74, 0x6f, 0x73, + 0x68, 0x69, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, + 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, + 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x29, 0x0a, 0x10, 0x64, + 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x48, 0x61, 0x73, 0x68, 0x12, 0x23, 0x0a, 0x0d, 0x66, 0x61, 0x6c, 0x6c, 0x62, 0x61, + 0x63, 0x6b, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x66, + 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x12, 0x1f, 0x0a, 0x0b, 0x63, + 0x6c, 0x74, 0x76, 0x5f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x0a, 0x63, 0x6c, 0x74, 0x76, 0x45, 0x78, 0x70, 0x69, 0x72, 0x79, 0x12, 0x31, 0x0a, 0x0b, + 0x72, 0x6f, 0x75, 0x74, 0x65, 0x5f, 0x68, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x10, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x48, + 0x69, 0x6e, 0x74, 0x52, 0x0a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x48, 0x69, 0x6e, 0x74, 0x73, 0x12, + 0x21, 0x0a, 0x0c, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, + 0x0b, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x41, 0x64, + 0x64, 0x72, 0x12, 0x19, 0x0a, 0x08, 0x6e, 0x75, 0x6d, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x0c, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x6e, 0x75, 0x6d, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x37, 0x0a, + 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x1b, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x61, 0x79, 0x52, 0x65, 0x71, 0x2e, 0x46, + 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x66, 0x65, + 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x12, 0x3e, 0x0a, 0x0d, 0x62, 0x6c, 0x69, 0x6e, 0x64, 0x65, + 0x64, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x73, 0x18, 0x0e, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, + 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x42, 0x6c, 0x69, 0x6e, 0x64, 0x65, 0x64, 0x50, 0x61, 0x79, + 0x6d, 0x65, 0x6e, 0x74, 0x50, 0x61, 0x74, 0x68, 0x52, 0x0c, 0x62, 0x6c, 0x69, 0x6e, 0x64, 0x65, + 0x64, 0x50, 0x61, 0x74, 0x68, 0x73, 0x1a, 0x4b, 0x0a, 0x0d, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, + 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, + 0x2e, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, + 0x02, 0x38, 0x01, 0x22, 0x59, 0x0a, 0x07, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x12, + 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x73, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, + 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x69, 0x73, 0x52, 0x65, 0x71, 0x75, 0x69, + 0x72, 0x65, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x69, 0x73, 0x5f, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x69, 0x73, 0x4b, 0x6e, 0x6f, 0x77, 0x6e, 0x22, 0x12, + 0x0a, 0x10, 0x46, 0x65, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x22, 0x95, 0x02, 0x0a, 0x10, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x46, 0x65, + 0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x1b, 0x0a, 0x07, 0x63, 0x68, 0x61, 0x6e, 0x5f, + 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, 0x30, 0x01, 0x52, 0x06, 0x63, 0x68, + 0x61, 0x6e, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, + 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x68, 0x61, + 0x6e, 0x6e, 0x65, 0x6c, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x22, 0x0a, 0x0d, 0x62, 0x61, 0x73, + 0x65, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x0b, 0x62, 0x61, 0x73, 0x65, 0x46, 0x65, 0x65, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x1e, 0x0a, + 0x0b, 0x66, 0x65, 0x65, 0x5f, 0x70, 0x65, 0x72, 0x5f, 0x6d, 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x09, 0x66, 0x65, 0x65, 0x50, 0x65, 0x72, 0x4d, 0x69, 0x6c, 0x12, 0x19, 0x0a, + 0x08, 0x66, 0x65, 0x65, 0x5f, 0x72, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x01, 0x52, + 0x07, 0x66, 0x65, 0x65, 0x52, 0x61, 0x74, 0x65, 0x12, 0x31, 0x0a, 0x15, 0x69, 0x6e, 0x62, 0x6f, + 0x75, 0x6e, 0x64, 0x5f, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x6d, 0x73, 0x61, + 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x12, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, + 0x42, 0x61, 0x73, 0x65, 0x46, 0x65, 0x65, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x2d, 0x0a, 0x13, 0x69, + 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x70, 0x65, 0x72, 0x5f, 0x6d, + 0x69, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, 0x10, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, + 0x64, 0x46, 0x65, 0x65, 0x50, 0x65, 0x72, 0x4d, 0x69, 0x6c, 0x22, 0xb5, 0x01, 0x0a, 0x11, 0x46, + 0x65, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x3a, 0x0a, 0x0c, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x66, 0x65, 0x65, 0x73, + 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, + 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x46, 0x65, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, + 0x0b, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x46, 0x65, 0x65, 0x73, 0x12, 0x1e, 0x0a, 0x0b, + 0x64, 0x61, 0x79, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x73, 0x75, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x04, 0x52, 0x09, 0x64, 0x61, 0x79, 0x46, 0x65, 0x65, 0x53, 0x75, 0x6d, 0x12, 0x20, 0x0a, 0x0c, + 0x77, 0x65, 0x65, 0x6b, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x73, 0x75, 0x6d, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x0a, 0x77, 0x65, 0x65, 0x6b, 0x46, 0x65, 0x65, 0x53, 0x75, 0x6d, 0x12, 0x22, + 0x0a, 0x0d, 0x6d, 0x6f, 0x6e, 0x74, 0x68, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x73, 0x75, 0x6d, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x6d, 0x6f, 0x6e, 0x74, 0x68, 0x46, 0x65, 0x65, 0x53, + 0x75, 0x6d, 0x22, 0x52, 0x0a, 0x0a, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x46, 0x65, 0x65, + 0x12, 0x22, 0x0a, 0x0d, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x6d, 0x73, 0x61, + 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x62, 0x61, 0x73, 0x65, 0x46, 0x65, 0x65, + 0x4d, 0x73, 0x61, 0x74, 0x12, 0x20, 0x0a, 0x0c, 0x66, 0x65, 0x65, 0x5f, 0x72, 0x61, 0x74, 0x65, + 0x5f, 0x70, 0x70, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x66, 0x65, 0x65, 0x52, + 0x61, 0x74, 0x65, 0x50, 0x70, 0x6d, 0x22, 0xda, 0x03, 0x0a, 0x13, 0x50, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, + 0x0a, 0x06, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, + 0x52, 0x06, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x12, 0x34, 0x0a, 0x0a, 0x63, 0x68, 0x61, 0x6e, + 0x5f, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x6c, + 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x50, 0x6f, 0x69, 0x6e, + 0x74, 0x48, 0x00, 0x52, 0x09, 0x63, 0x68, 0x61, 0x6e, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x22, + 0x0a, 0x0d, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x62, 0x61, 0x73, 0x65, 0x46, 0x65, 0x65, 0x4d, 0x73, + 0x61, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x66, 0x65, 0x65, 0x5f, 0x72, 0x61, 0x74, 0x65, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x01, 0x52, 0x07, 0x66, 0x65, 0x65, 0x52, 0x61, 0x74, 0x65, 0x12, 0x20, 0x0a, + 0x0c, 0x66, 0x65, 0x65, 0x5f, 0x72, 0x61, 0x74, 0x65, 0x5f, 0x70, 0x70, 0x6d, 0x18, 0x09, 0x20, + 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x66, 0x65, 0x65, 0x52, 0x61, 0x74, 0x65, 0x50, 0x70, 0x6d, 0x12, + 0x26, 0x0a, 0x0f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x64, 0x65, 0x6c, + 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0d, 0x74, 0x69, 0x6d, 0x65, 0x4c, 0x6f, + 0x63, 0x6b, 0x44, 0x65, 0x6c, 0x74, 0x61, 0x12, 0x22, 0x0a, 0x0d, 0x6d, 0x61, 0x78, 0x5f, 0x68, + 0x74, 0x6c, 0x63, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, + 0x6d, 0x61, 0x78, 0x48, 0x74, 0x6c, 0x63, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x22, 0x0a, 0x0d, 0x6d, + 0x69, 0x6e, 0x5f, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x0b, 0x6d, 0x69, 0x6e, 0x48, 0x74, 0x6c, 0x63, 0x4d, 0x73, 0x61, 0x74, 0x12, + 0x35, 0x0a, 0x17, 0x6d, 0x69, 0x6e, 0x5f, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x6d, 0x73, 0x61, 0x74, + 0x5f, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x14, 0x6d, 0x69, 0x6e, 0x48, 0x74, 0x6c, 0x63, 0x4d, 0x73, 0x61, 0x74, 0x53, 0x70, 0x65, + 0x63, 0x69, 0x66, 0x69, 0x65, 0x64, 0x12, 0x32, 0x0a, 0x0b, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, + 0x64, 0x5f, 0x66, 0x65, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6c, 0x6e, + 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x46, 0x65, 0x65, 0x52, 0x0a, + 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x46, 0x65, 0x65, 0x12, 0x2e, 0x0a, 0x13, 0x63, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x5f, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x5f, 0x65, 0x64, 0x67, + 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, + 0x69, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x45, 0x64, 0x67, 0x65, 0x42, 0x07, 0x0a, 0x05, 0x73, 0x63, + 0x6f, 0x70, 0x65, 0x22, 0x8c, 0x01, 0x0a, 0x0c, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x12, 0x2b, 0x0a, 0x08, 0x6f, 0x75, 0x74, 0x70, 0x6f, 0x69, 0x6e, 0x74, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4f, + 0x75, 0x74, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x08, 0x6f, 0x75, 0x74, 0x70, 0x6f, 0x69, 0x6e, + 0x74, 0x12, 0x2c, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x14, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, + 0x21, 0x0a, 0x0c, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x72, 0x72, + 0x6f, 0x72, 0x22, 0x52, 0x0a, 0x14, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a, 0x0a, 0x0e, 0x66, 0x61, + 0x69, 0x6c, 0x65, 0x64, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x61, 0x69, 0x6c, 0x65, + 0x64, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x0d, 0x66, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x22, 0xa1, 0x02, 0x0a, 0x18, 0x46, 0x6f, 0x72, 0x77, 0x61, + 0x72, 0x64, 0x69, 0x6e, 0x67, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, + 0x6d, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x21, 0x0a, + 0x0c, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x5f, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x4f, 0x66, 0x66, 0x73, 0x65, 0x74, + 0x12, 0x24, 0x0a, 0x0e, 0x6e, 0x75, 0x6d, 0x5f, 0x6d, 0x61, 0x78, 0x5f, 0x65, 0x76, 0x65, 0x6e, + 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0c, 0x6e, 0x75, 0x6d, 0x4d, 0x61, 0x78, + 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x2a, 0x0a, 0x11, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x61, + 0x6c, 0x69, 0x61, 0x73, 0x5f, 0x6c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x0f, 0x70, 0x65, 0x65, 0x72, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x4c, 0x6f, 0x6f, 0x6b, + 0x75, 0x70, 0x12, 0x2a, 0x0a, 0x11, 0x69, 0x6e, 0x63, 0x6f, 0x6d, 0x69, 0x6e, 0x67, 0x5f, 0x63, + 0x68, 0x61, 0x6e, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x04, 0x52, 0x0f, 0x69, + 0x6e, 0x63, 0x6f, 0x6d, 0x69, 0x6e, 0x67, 0x43, 0x68, 0x61, 0x6e, 0x49, 0x64, 0x73, 0x12, 0x2a, + 0x0a, 0x11, 0x6f, 0x75, 0x74, 0x67, 0x6f, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x5f, + 0x69, 0x64, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x04, 0x52, 0x0f, 0x6f, 0x75, 0x74, 0x67, 0x6f, + 0x69, 0x6e, 0x67, 0x43, 0x68, 0x61, 0x6e, 0x49, 0x64, 0x73, 0x22, 0x8d, 0x04, 0x0a, 0x0f, 0x46, + 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x20, + 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x04, 0x42, 0x02, 0x18, 0x01, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x12, 0x20, 0x0a, 0x0a, 0x63, 0x68, 0x61, 0x6e, 0x5f, 0x69, 0x64, 0x5f, 0x69, 0x6e, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, 0x30, 0x01, 0x52, 0x08, 0x63, 0x68, 0x61, 0x6e, 0x49, 0x64, + 0x49, 0x6e, 0x12, 0x22, 0x0a, 0x0b, 0x63, 0x68, 0x61, 0x6e, 0x5f, 0x69, 0x64, 0x5f, 0x6f, 0x75, + 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, 0x30, 0x01, 0x52, 0x09, 0x63, 0x68, 0x61, + 0x6e, 0x49, 0x64, 0x4f, 0x75, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x61, 0x6d, 0x74, 0x5f, 0x69, 0x6e, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x61, 0x6d, 0x74, 0x49, 0x6e, 0x12, 0x17, 0x0a, + 0x07, 0x61, 0x6d, 0x74, 0x5f, 0x6f, 0x75, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, + 0x61, 0x6d, 0x74, 0x4f, 0x75, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x66, 0x65, 0x65, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x04, 0x52, 0x03, 0x66, 0x65, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x66, 0x65, 0x65, 0x5f, + 0x6d, 0x73, 0x61, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x66, 0x65, 0x65, 0x4d, + 0x73, 0x61, 0x74, 0x12, 0x1e, 0x0a, 0x0b, 0x61, 0x6d, 0x74, 0x5f, 0x69, 0x6e, 0x5f, 0x6d, 0x73, + 0x61, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x61, 0x6d, 0x74, 0x49, 0x6e, 0x4d, + 0x73, 0x61, 0x74, 0x12, 0x20, 0x0a, 0x0c, 0x61, 0x6d, 0x74, 0x5f, 0x6f, 0x75, 0x74, 0x5f, 0x6d, + 0x73, 0x61, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x61, 0x6d, 0x74, 0x4f, 0x75, + 0x74, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x5f, 0x6e, 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x74, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x4e, 0x73, 0x12, 0x22, 0x0a, 0x0d, 0x70, 0x65, 0x65, 0x72, + 0x5f, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x5f, 0x69, 0x6e, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0b, 0x70, 0x65, 0x65, 0x72, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x49, 0x6e, 0x12, 0x24, 0x0a, 0x0e, + 0x70, 0x65, 0x65, 0x72, 0x5f, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x5f, 0x6f, 0x75, 0x74, 0x18, 0x0d, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x65, 0x65, 0x72, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x4f, + 0x75, 0x74, 0x12, 0x2d, 0x0a, 0x10, 0x69, 0x6e, 0x63, 0x6f, 0x6d, 0x69, 0x6e, 0x67, 0x5f, 0x68, + 0x74, 0x6c, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x04, 0x48, 0x00, 0x52, 0x0e, + 0x69, 0x6e, 0x63, 0x6f, 0x6d, 0x69, 0x6e, 0x67, 0x48, 0x74, 0x6c, 0x63, 0x49, 0x64, 0x88, 0x01, + 0x01, 0x12, 0x2d, 0x0a, 0x10, 0x6f, 0x75, 0x74, 0x67, 0x6f, 0x69, 0x6e, 0x67, 0x5f, 0x68, 0x74, + 0x6c, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x04, 0x48, 0x01, 0x52, 0x0e, 0x6f, + 0x75, 0x74, 0x67, 0x6f, 0x69, 0x6e, 0x67, 0x48, 0x74, 0x6c, 0x63, 0x49, 0x64, 0x88, 0x01, 0x01, + 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x69, 0x6e, 0x63, 0x6f, 0x6d, 0x69, 0x6e, 0x67, 0x5f, 0x68, 0x74, + 0x6c, 0x63, 0x5f, 0x69, 0x64, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x6f, 0x75, 0x74, 0x67, 0x6f, 0x69, + 0x6e, 0x67, 0x5f, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x69, 0x64, 0x22, 0x8c, 0x01, 0x0a, 0x19, 0x46, + 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x11, 0x66, 0x6f, 0x72, 0x77, + 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x6f, 0x72, 0x77, + 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x10, 0x66, 0x6f, 0x72, + 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x2a, 0x0a, + 0x11, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x6e, 0x64, + 0x65, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0f, 0x6c, 0x61, 0x73, 0x74, 0x4f, 0x66, + 0x66, 0x73, 0x65, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x22, 0x50, 0x0a, 0x1a, 0x45, 0x78, 0x70, + 0x6f, 0x72, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x32, 0x0a, 0x0a, 0x63, 0x68, 0x61, 0x6e, 0x5f, + 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x6c, 0x6e, + 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x50, 0x6f, 0x69, 0x6e, 0x74, + 0x52, 0x09, 0x63, 0x68, 0x61, 0x6e, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x22, 0x64, 0x0a, 0x0d, 0x43, + 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x12, 0x32, 0x0a, 0x0a, + 0x63, 0x68, 0x61, 0x6e, 0x5f, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x13, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, + 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x09, 0x63, 0x68, 0x61, 0x6e, 0x50, 0x6f, 0x69, 0x6e, 0x74, + 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x68, 0x61, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x63, 0x68, 0x61, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x75, + 0x70, 0x22, 0x73, 0x0a, 0x0f, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x43, 0x68, 0x61, 0x6e, 0x42, 0x61, + 0x63, 0x6b, 0x75, 0x70, 0x12, 0x34, 0x0a, 0x0b, 0x63, 0x68, 0x61, 0x6e, 0x5f, 0x70, 0x6f, 0x69, + 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x6c, 0x6e, 0x72, 0x70, + 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x0a, + 0x63, 0x68, 0x61, 0x6e, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x2a, 0x0a, 0x11, 0x6d, 0x75, + 0x6c, 0x74, 0x69, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x43, 0x68, 0x61, 0x6e, + 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x22, 0x19, 0x0a, 0x17, 0x43, 0x68, 0x61, 0x6e, 0x42, 0x61, + 0x63, 0x6b, 0x75, 0x70, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x22, 0x9f, 0x01, 0x0a, 0x12, 0x43, 0x68, 0x61, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, + 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x45, 0x0a, 0x13, 0x73, 0x69, 0x6e, 0x67, + 0x6c, 0x65, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, + 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x52, 0x11, 0x73, 0x69, + 0x6e, 0x67, 0x6c, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x12, + 0x42, 0x0a, 0x11, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x5f, 0x62, 0x61, + 0x63, 0x6b, 0x75, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6c, 0x6e, 0x72, + 0x70, 0x63, 0x2e, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x43, 0x68, 0x61, 0x6e, 0x42, 0x61, 0x63, 0x6b, + 0x75, 0x70, 0x52, 0x0f, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x43, 0x68, 0x61, 0x6e, 0x42, 0x61, 0x63, + 0x6b, 0x75, 0x70, 0x22, 0x49, 0x0a, 0x0e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x42, 0x61, + 0x63, 0x6b, 0x75, 0x70, 0x73, 0x12, 0x37, 0x0a, 0x0c, 0x63, 0x68, 0x61, 0x6e, 0x5f, 0x62, 0x61, + 0x63, 0x6b, 0x75, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6c, 0x6e, + 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x42, 0x61, 0x63, 0x6b, 0x75, + 0x70, 0x52, 0x0b, 0x63, 0x68, 0x61, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x22, 0x8e, + 0x01, 0x0a, 0x18, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x42, 0x61, + 0x63, 0x6b, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3a, 0x0a, 0x0c, 0x63, + 0x68, 0x61, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x15, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, + 0x6c, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x48, 0x00, 0x52, 0x0b, 0x63, 0x68, 0x61, 0x6e, + 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x12, 0x2c, 0x0a, 0x11, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0c, 0x52, 0x0f, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x43, 0x68, 0x61, 0x6e, 0x42, 0x61, 0x63, - 0x6b, 0x75, 0x70, 0x22, 0x19, 0x0a, 0x17, 0x43, 0x68, 0x61, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x75, - 0x70, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x9f, - 0x01, 0x0a, 0x12, 0x43, 0x68, 0x61, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x53, 0x6e, 0x61, - 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x45, 0x0a, 0x13, 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x5f, - 0x63, 0x68, 0x61, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, - 0x65, 0x6c, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x52, 0x11, 0x73, 0x69, 0x6e, 0x67, 0x6c, - 0x65, 0x43, 0x68, 0x61, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x12, 0x42, 0x0a, 0x11, - 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x75, - 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, - 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x43, 0x68, 0x61, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x52, - 0x0f, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x43, 0x68, 0x61, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, - 0x22, 0x49, 0x0a, 0x0e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x42, 0x61, 0x63, 0x6b, 0x75, - 0x70, 0x73, 0x12, 0x37, 0x0a, 0x0c, 0x63, 0x68, 0x61, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x75, - 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, - 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x52, 0x0b, - 0x63, 0x68, 0x61, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x22, 0x8e, 0x01, 0x0a, 0x18, - 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x75, - 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3a, 0x0a, 0x0c, 0x63, 0x68, 0x61, 0x6e, - 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, - 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x42, 0x61, - 0x63, 0x6b, 0x75, 0x70, 0x73, 0x48, 0x00, 0x52, 0x0b, 0x63, 0x68, 0x61, 0x6e, 0x42, 0x61, 0x63, - 0x6b, 0x75, 0x70, 0x73, 0x12, 0x2c, 0x0a, 0x11, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x5f, 0x63, 0x68, - 0x61, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x48, - 0x00, 0x52, 0x0f, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x43, 0x68, 0x61, 0x6e, 0x42, 0x61, 0x63, 0x6b, - 0x75, 0x70, 0x42, 0x08, 0x0a, 0x06, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x22, 0x3a, 0x0a, 0x15, - 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x6e, 0x75, 0x6d, 0x5f, 0x72, 0x65, 0x73, - 0x74, 0x6f, 0x72, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x6e, 0x75, 0x6d, - 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x22, 0x1b, 0x0a, 0x19, 0x43, 0x68, 0x61, 0x6e, - 0x6e, 0x65, 0x6c, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x3b, 0x0a, 0x18, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x43, - 0x68, 0x61, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x68, 0x61, 0x6e, 0x5f, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x68, 0x61, 0x6e, 0x50, 0x6f, 0x69, 0x6e, - 0x74, 0x73, 0x22, 0x44, 0x0a, 0x12, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x50, 0x65, - 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x6e, 0x74, 0x69, - 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, - 0x12, 0x16, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xb0, 0x01, 0x0a, 0x13, 0x42, 0x61, 0x6b, - 0x65, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x3b, 0x0a, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x61, - 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, - 0x52, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1e, 0x0a, - 0x0b, 0x72, 0x6f, 0x6f, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x04, 0x52, 0x09, 0x72, 0x6f, 0x6f, 0x74, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x12, 0x3c, 0x0a, - 0x1a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, - 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x18, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, - 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x32, 0x0a, 0x14, 0x42, - 0x61, 0x6b, 0x65, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x6d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x22, - 0x18, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x49, - 0x44, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3b, 0x0a, 0x17, 0x4c, 0x69, 0x73, - 0x74, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x49, 0x44, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x20, 0x0a, 0x0c, 0x72, 0x6f, 0x6f, 0x74, 0x5f, 0x6b, 0x65, 0x79, - 0x5f, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x04, 0x52, 0x0a, 0x72, 0x6f, 0x6f, 0x74, - 0x4b, 0x65, 0x79, 0x49, 0x64, 0x73, 0x22, 0x39, 0x0a, 0x17, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x49, 0x44, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x1e, 0x0a, 0x0b, 0x72, 0x6f, 0x6f, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x72, 0x6f, 0x6f, 0x74, 0x4b, 0x65, 0x79, 0x49, - 0x64, 0x22, 0x34, 0x0a, 0x18, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x61, 0x72, - 0x6f, 0x6f, 0x6e, 0x49, 0x44, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, - 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, - 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x22, 0x55, 0x0a, 0x16, 0x4d, 0x61, 0x63, 0x61, 0x72, - 0x6f, 0x6f, 0x6e, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x73, - 0x74, 0x12, 0x3b, 0x0a, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4d, - 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, - 0x6e, 0x52, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x18, - 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xe4, 0x01, 0x0a, 0x17, 0x4c, 0x69, 0x73, - 0x74, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x64, 0x0a, 0x12, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x70, - 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x35, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x65, 0x72, - 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, - 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x11, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x50, - 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x1a, 0x63, 0x0a, 0x16, 0x4d, 0x65, - 0x74, 0x68, 0x6f, 0x64, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x45, - 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x33, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x61, + 0x28, 0x0c, 0x48, 0x00, 0x52, 0x0f, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x43, 0x68, 0x61, 0x6e, 0x42, + 0x61, 0x63, 0x6b, 0x75, 0x70, 0x42, 0x08, 0x0a, 0x06, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x22, + 0x3a, 0x0a, 0x15, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x6e, 0x75, 0x6d, 0x5f, + 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, + 0x6e, 0x75, 0x6d, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x22, 0x1b, 0x0a, 0x19, 0x43, + 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x53, 0x75, 0x62, 0x73, + 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x3b, 0x0a, 0x18, 0x56, 0x65, 0x72, 0x69, + 0x66, 0x79, 0x43, 0x68, 0x61, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x68, 0x61, 0x6e, 0x5f, 0x70, 0x6f, 0x69, + 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x68, 0x61, 0x6e, 0x50, + 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x22, 0x44, 0x0a, 0x12, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, + 0x6e, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x65, + 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x65, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xb0, 0x01, 0x0a, 0x13, + 0x42, 0x61, 0x6b, 0x65, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x3b, 0x0a, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, + 0x2e, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, + 0x12, 0x1e, 0x0a, 0x0b, 0x72, 0x6f, 0x6f, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x72, 0x6f, 0x6f, 0x74, 0x4b, 0x65, 0x79, 0x49, 0x64, + 0x12, 0x3c, 0x0a, 0x1a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x5f, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x18, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x45, 0x78, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x32, + 0x0a, 0x14, 0x42, 0x61, 0x6b, 0x65, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x6d, 0x61, 0x63, 0x61, 0x72, 0x6f, + 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6d, 0x61, 0x63, 0x61, 0x72, 0x6f, + 0x6f, 0x6e, 0x22, 0x18, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, + 0x6f, 0x6e, 0x49, 0x44, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3b, 0x0a, 0x17, + 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x49, 0x44, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x20, 0x0a, 0x0c, 0x72, 0x6f, 0x6f, 0x74, 0x5f, + 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x04, 0x52, 0x0a, 0x72, + 0x6f, 0x6f, 0x74, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x73, 0x22, 0x39, 0x0a, 0x17, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x49, 0x44, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x0b, 0x72, 0x6f, 0x6f, 0x74, 0x5f, 0x6b, 0x65, 0x79, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x72, 0x6f, 0x6f, 0x74, 0x4b, + 0x65, 0x79, 0x49, 0x64, 0x22, 0x34, 0x0a, 0x18, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, + 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x49, 0x44, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x18, 0x0a, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x22, 0x55, 0x0a, 0x16, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, - 0x4c, 0x69, 0x73, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, - 0xcc, 0x08, 0x0a, 0x07, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x12, 0x2e, 0x0a, 0x04, 0x63, - 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x6c, 0x6e, 0x72, 0x70, - 0x63, 0x2e, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x2e, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, - 0x65, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x3b, 0x0a, 0x0e, 0x63, - 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, - 0x6e, 0x65, 0x6c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x0d, 0x63, 0x68, 0x61, 0x6e, 0x6e, - 0x65, 0x6c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x68, 0x74, 0x6c, 0x63, - 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x68, 0x74, 0x6c, - 0x63, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x22, 0x0a, 0x0d, 0x6f, 0x6e, 0x69, 0x6f, 0x6e, 0x5f, 0x73, - 0x68, 0x61, 0x5f, 0x32, 0x35, 0x36, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x6f, 0x6e, - 0x69, 0x6f, 0x6e, 0x53, 0x68, 0x61, 0x32, 0x35, 0x36, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6c, 0x74, - 0x76, 0x5f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, - 0x63, 0x6c, 0x74, 0x76, 0x45, 0x78, 0x70, 0x69, 0x72, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x6c, - 0x61, 0x67, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x66, 0x6c, 0x61, 0x67, 0x73, - 0x12, 0x30, 0x0a, 0x14, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x5f, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x12, - 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x64, - 0x65, 0x78, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x09, 0x20, 0x01, - 0x28, 0x0d, 0x52, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x22, 0x8b, 0x06, 0x0a, 0x0b, 0x46, - 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x0c, 0x0a, 0x08, 0x52, 0x45, - 0x53, 0x45, 0x52, 0x56, 0x45, 0x44, 0x10, 0x00, 0x12, 0x28, 0x0a, 0x24, 0x49, 0x4e, 0x43, 0x4f, - 0x52, 0x52, 0x45, 0x43, 0x54, 0x5f, 0x4f, 0x52, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, - 0x5f, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x44, 0x45, 0x54, 0x41, 0x49, 0x4c, 0x53, - 0x10, 0x01, 0x12, 0x1c, 0x0a, 0x18, 0x49, 0x4e, 0x43, 0x4f, 0x52, 0x52, 0x45, 0x43, 0x54, 0x5f, - 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x41, 0x4d, 0x4f, 0x55, 0x4e, 0x54, 0x10, 0x02, - 0x12, 0x1f, 0x0a, 0x1b, 0x46, 0x49, 0x4e, 0x41, 0x4c, 0x5f, 0x49, 0x4e, 0x43, 0x4f, 0x52, 0x52, - 0x45, 0x43, 0x54, 0x5f, 0x43, 0x4c, 0x54, 0x56, 0x5f, 0x45, 0x58, 0x50, 0x49, 0x52, 0x59, 0x10, - 0x03, 0x12, 0x1f, 0x0a, 0x1b, 0x46, 0x49, 0x4e, 0x41, 0x4c, 0x5f, 0x49, 0x4e, 0x43, 0x4f, 0x52, - 0x52, 0x45, 0x43, 0x54, 0x5f, 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x41, 0x4d, 0x4f, 0x55, 0x4e, 0x54, - 0x10, 0x04, 0x12, 0x19, 0x0a, 0x15, 0x46, 0x49, 0x4e, 0x41, 0x4c, 0x5f, 0x45, 0x58, 0x50, 0x49, - 0x52, 0x59, 0x5f, 0x54, 0x4f, 0x4f, 0x5f, 0x53, 0x4f, 0x4f, 0x4e, 0x10, 0x05, 0x12, 0x11, 0x0a, - 0x0d, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x5f, 0x52, 0x45, 0x41, 0x4c, 0x4d, 0x10, 0x06, - 0x12, 0x13, 0x0a, 0x0f, 0x45, 0x58, 0x50, 0x49, 0x52, 0x59, 0x5f, 0x54, 0x4f, 0x4f, 0x5f, 0x53, - 0x4f, 0x4f, 0x4e, 0x10, 0x07, 0x12, 0x19, 0x0a, 0x15, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, - 0x5f, 0x4f, 0x4e, 0x49, 0x4f, 0x4e, 0x5f, 0x56, 0x45, 0x52, 0x53, 0x49, 0x4f, 0x4e, 0x10, 0x08, - 0x12, 0x16, 0x0a, 0x12, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x5f, 0x4f, 0x4e, 0x49, 0x4f, - 0x4e, 0x5f, 0x48, 0x4d, 0x41, 0x43, 0x10, 0x09, 0x12, 0x15, 0x0a, 0x11, 0x49, 0x4e, 0x56, 0x41, - 0x4c, 0x49, 0x44, 0x5f, 0x4f, 0x4e, 0x49, 0x4f, 0x4e, 0x5f, 0x4b, 0x45, 0x59, 0x10, 0x0a, 0x12, - 0x18, 0x0a, 0x14, 0x41, 0x4d, 0x4f, 0x55, 0x4e, 0x54, 0x5f, 0x42, 0x45, 0x4c, 0x4f, 0x57, 0x5f, - 0x4d, 0x49, 0x4e, 0x49, 0x4d, 0x55, 0x4d, 0x10, 0x0b, 0x12, 0x14, 0x0a, 0x10, 0x46, 0x45, 0x45, - 0x5f, 0x49, 0x4e, 0x53, 0x55, 0x46, 0x46, 0x49, 0x43, 0x49, 0x45, 0x4e, 0x54, 0x10, 0x0c, 0x12, - 0x19, 0x0a, 0x15, 0x49, 0x4e, 0x43, 0x4f, 0x52, 0x52, 0x45, 0x43, 0x54, 0x5f, 0x43, 0x4c, 0x54, - 0x56, 0x5f, 0x45, 0x58, 0x50, 0x49, 0x52, 0x59, 0x10, 0x0d, 0x12, 0x14, 0x0a, 0x10, 0x43, 0x48, - 0x41, 0x4e, 0x4e, 0x45, 0x4c, 0x5f, 0x44, 0x49, 0x53, 0x41, 0x42, 0x4c, 0x45, 0x44, 0x10, 0x0e, - 0x12, 0x1d, 0x0a, 0x19, 0x54, 0x45, 0x4d, 0x50, 0x4f, 0x52, 0x41, 0x52, 0x59, 0x5f, 0x43, 0x48, - 0x41, 0x4e, 0x4e, 0x45, 0x4c, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x10, 0x0f, 0x12, - 0x21, 0x0a, 0x1d, 0x52, 0x45, 0x51, 0x55, 0x49, 0x52, 0x45, 0x44, 0x5f, 0x4e, 0x4f, 0x44, 0x45, - 0x5f, 0x46, 0x45, 0x41, 0x54, 0x55, 0x52, 0x45, 0x5f, 0x4d, 0x49, 0x53, 0x53, 0x49, 0x4e, 0x47, - 0x10, 0x10, 0x12, 0x24, 0x0a, 0x20, 0x52, 0x45, 0x51, 0x55, 0x49, 0x52, 0x45, 0x44, 0x5f, 0x43, - 0x48, 0x41, 0x4e, 0x4e, 0x45, 0x4c, 0x5f, 0x46, 0x45, 0x41, 0x54, 0x55, 0x52, 0x45, 0x5f, 0x4d, - 0x49, 0x53, 0x53, 0x49, 0x4e, 0x47, 0x10, 0x11, 0x12, 0x15, 0x0a, 0x11, 0x55, 0x4e, 0x4b, 0x4e, - 0x4f, 0x57, 0x4e, 0x5f, 0x4e, 0x45, 0x58, 0x54, 0x5f, 0x50, 0x45, 0x45, 0x52, 0x10, 0x12, 0x12, - 0x1a, 0x0a, 0x16, 0x54, 0x45, 0x4d, 0x50, 0x4f, 0x52, 0x41, 0x52, 0x59, 0x5f, 0x4e, 0x4f, 0x44, - 0x45, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x10, 0x13, 0x12, 0x1a, 0x0a, 0x16, 0x50, - 0x45, 0x52, 0x4d, 0x41, 0x4e, 0x45, 0x4e, 0x54, 0x5f, 0x4e, 0x4f, 0x44, 0x45, 0x5f, 0x46, 0x41, - 0x49, 0x4c, 0x55, 0x52, 0x45, 0x10, 0x14, 0x12, 0x1d, 0x0a, 0x19, 0x50, 0x45, 0x52, 0x4d, 0x41, - 0x4e, 0x45, 0x4e, 0x54, 0x5f, 0x43, 0x48, 0x41, 0x4e, 0x4e, 0x45, 0x4c, 0x5f, 0x46, 0x41, 0x49, - 0x4c, 0x55, 0x52, 0x45, 0x10, 0x15, 0x12, 0x12, 0x0a, 0x0e, 0x45, 0x58, 0x50, 0x49, 0x52, 0x59, - 0x5f, 0x54, 0x4f, 0x4f, 0x5f, 0x46, 0x41, 0x52, 0x10, 0x16, 0x12, 0x0f, 0x0a, 0x0b, 0x4d, 0x50, - 0x50, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x10, 0x17, 0x12, 0x19, 0x0a, 0x15, 0x49, - 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x5f, 0x4f, 0x4e, 0x49, 0x4f, 0x4e, 0x5f, 0x50, 0x41, 0x59, - 0x4c, 0x4f, 0x41, 0x44, 0x10, 0x18, 0x12, 0x1a, 0x0a, 0x16, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, - 0x44, 0x5f, 0x4f, 0x4e, 0x49, 0x4f, 0x4e, 0x5f, 0x42, 0x4c, 0x49, 0x4e, 0x44, 0x49, 0x4e, 0x47, - 0x10, 0x19, 0x12, 0x15, 0x0a, 0x10, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x5f, 0x46, - 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x10, 0xe5, 0x07, 0x12, 0x14, 0x0a, 0x0f, 0x55, 0x4e, 0x4b, - 0x4e, 0x4f, 0x57, 0x4e, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x10, 0xe6, 0x07, 0x12, - 0x17, 0x0a, 0x12, 0x55, 0x4e, 0x52, 0x45, 0x41, 0x44, 0x41, 0x42, 0x4c, 0x45, 0x5f, 0x46, 0x41, - 0x49, 0x4c, 0x55, 0x52, 0x45, 0x10, 0xe7, 0x07, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x22, 0xb3, - 0x03, 0x0a, 0x0d, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x1d, - 0x0a, 0x0a, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0c, 0x52, 0x09, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x48, 0x61, 0x73, 0x68, 0x12, 0x1b, 0x0a, - 0x07, 0x63, 0x68, 0x61, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, - 0x30, 0x01, 0x52, 0x06, 0x63, 0x68, 0x61, 0x6e, 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, - 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, 0x74, - 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x23, 0x0a, 0x0d, 0x6d, 0x65, 0x73, 0x73, - 0x61, 0x67, 0x65, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0d, 0x52, - 0x0c, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x12, 0x23, 0x0a, - 0x0d, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0c, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x46, 0x6c, 0x61, - 0x67, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, - 0x64, 0x65, 0x6c, 0x74, 0x61, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0d, 0x74, 0x69, 0x6d, - 0x65, 0x4c, 0x6f, 0x63, 0x6b, 0x44, 0x65, 0x6c, 0x74, 0x61, 0x12, 0x2a, 0x0a, 0x11, 0x68, 0x74, - 0x6c, 0x63, 0x5f, 0x6d, 0x69, 0x6e, 0x69, 0x6d, 0x75, 0x6d, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, - 0x07, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0f, 0x68, 0x74, 0x6c, 0x63, 0x4d, 0x69, 0x6e, 0x69, 0x6d, - 0x75, 0x6d, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x66, - 0x65, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x62, 0x61, 0x73, 0x65, 0x46, 0x65, - 0x65, 0x12, 0x19, 0x0a, 0x08, 0x66, 0x65, 0x65, 0x5f, 0x72, 0x61, 0x74, 0x65, 0x18, 0x09, 0x20, - 0x01, 0x28, 0x0d, 0x52, 0x07, 0x66, 0x65, 0x65, 0x52, 0x61, 0x74, 0x65, 0x12, 0x2a, 0x0a, 0x11, - 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x6d, 0x61, 0x78, 0x69, 0x6d, 0x75, 0x6d, 0x5f, 0x6d, 0x73, 0x61, - 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0f, 0x68, 0x74, 0x6c, 0x63, 0x4d, 0x61, 0x78, - 0x69, 0x6d, 0x75, 0x6d, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x2a, 0x0a, 0x11, 0x65, 0x78, 0x74, 0x72, - 0x61, 0x5f, 0x6f, 0x70, 0x61, 0x71, 0x75, 0x65, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x0c, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x65, 0x78, 0x74, 0x72, 0x61, 0x4f, 0x70, 0x61, 0x71, 0x75, 0x65, - 0x44, 0x61, 0x74, 0x61, 0x22, 0x5d, 0x0a, 0x0a, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, - 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x74, 0x6f, 0x72, - 0x61, 0x67, 0x65, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x74, 0x6f, - 0x72, 0x61, 0x67, 0x65, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x03, 0x6f, 0x70, 0x73, 0x18, 0x03, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x09, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4f, 0x70, 0x52, 0x03, - 0x6f, 0x70, 0x73, 0x22, 0x36, 0x0a, 0x02, 0x4f, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x6e, 0x74, - 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, - 0x79, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, - 0x28, 0x09, 0x52, 0x07, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xdd, 0x01, 0x0a, 0x13, - 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4d, 0x61, 0x63, 0x50, 0x65, 0x72, 0x6d, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x6d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x6d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x12, - 0x3b, 0x0a, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x61, 0x63, - 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, - 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1e, 0x0a, 0x0a, - 0x66, 0x75, 0x6c, 0x6c, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x4d, 0x0a, 0x24, - 0x63, 0x68, 0x65, 0x63, 0x6b, 0x5f, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x70, 0x65, - 0x72, 0x6d, 0x73, 0x5f, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x66, 0x75, 0x6c, 0x6c, 0x5f, 0x6d, 0x65, - 0x74, 0x68, 0x6f, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1f, 0x63, 0x68, 0x65, 0x63, - 0x6b, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x50, 0x65, 0x72, 0x6d, 0x73, 0x46, 0x72, 0x6f, - 0x6d, 0x46, 0x75, 0x6c, 0x6c, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x22, 0x2c, 0x0a, 0x14, 0x43, - 0x68, 0x65, 0x63, 0x6b, 0x4d, 0x61, 0x63, 0x50, 0x65, 0x72, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x22, 0xa4, 0x04, 0x0a, 0x14, 0x52, 0x50, - 0x43, 0x4d, 0x69, 0x64, 0x64, 0x6c, 0x65, 0x77, 0x61, 0x72, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, - 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x72, 0x61, 0x77, 0x5f, 0x6d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, - 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x72, 0x61, 0x77, 0x4d, 0x61, 0x63, 0x61, - 0x72, 0x6f, 0x6f, 0x6e, 0x12, 0x36, 0x0a, 0x17, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x63, - 0x61, 0x76, 0x65, 0x61, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x43, 0x61, 0x76, - 0x65, 0x61, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x0b, - 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x11, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, - 0x41, 0x75, 0x74, 0x68, 0x48, 0x00, 0x52, 0x0a, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x41, 0x75, - 0x74, 0x68, 0x12, 0x2d, 0x0a, 0x07, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x50, 0x43, 0x4d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x48, 0x00, 0x52, 0x07, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x2f, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x50, 0x43, 0x4d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x48, 0x00, 0x52, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x23, 0x0a, 0x0c, 0x72, 0x65, 0x67, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, - 0x74, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0b, 0x72, 0x65, 0x67, 0x43, - 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x15, 0x0a, 0x06, 0x6d, 0x73, 0x67, 0x5f, 0x69, - 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x6d, 0x73, 0x67, 0x49, 0x64, 0x12, 0x55, - 0x0a, 0x0e, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x70, 0x61, 0x69, 0x72, 0x73, - 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x52, - 0x50, 0x43, 0x4d, 0x69, 0x64, 0x64, 0x6c, 0x65, 0x77, 0x61, 0x72, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x50, 0x61, 0x69, 0x72, - 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0d, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x50, 0x61, 0x69, 0x72, 0x73, 0x1a, 0x57, 0x0a, 0x12, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, - 0x61, 0x50, 0x61, 0x69, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, - 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2b, 0x0a, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6c, - 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x73, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x10, - 0x0a, 0x0e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, - 0x22, 0x28, 0x0a, 0x0e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, - 0x28, 0x09, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x22, 0x34, 0x0a, 0x0a, 0x53, 0x74, - 0x72, 0x65, 0x61, 0x6d, 0x41, 0x75, 0x74, 0x68, 0x12, 0x26, 0x0a, 0x0f, 0x6d, 0x65, 0x74, 0x68, - 0x6f, 0x64, 0x5f, 0x66, 0x75, 0x6c, 0x6c, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0d, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x46, 0x75, 0x6c, 0x6c, 0x55, 0x72, 0x69, - 0x22, 0xab, 0x01, 0x0a, 0x0a, 0x52, 0x50, 0x43, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, - 0x26, 0x0a, 0x0f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x66, 0x75, 0x6c, 0x6c, 0x5f, 0x75, - 0x72, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, - 0x46, 0x75, 0x6c, 0x6c, 0x55, 0x72, 0x69, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x65, 0x61, - 0x6d, 0x5f, 0x72, 0x70, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x74, 0x72, - 0x65, 0x61, 0x6d, 0x52, 0x70, 0x63, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, - 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, - 0x61, 0x6d, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, - 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x69, - 0x7a, 0x65, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x69, 0x73, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x69, 0x73, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x22, 0xc0, - 0x01, 0x0a, 0x15, 0x52, 0x50, 0x43, 0x4d, 0x69, 0x64, 0x64, 0x6c, 0x65, 0x77, 0x61, 0x72, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x0a, 0x72, 0x65, 0x66, 0x5f, - 0x6d, 0x73, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x72, 0x65, - 0x66, 0x4d, 0x73, 0x67, 0x49, 0x64, 0x12, 0x3b, 0x0a, 0x08, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, - 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, - 0x2e, 0x4d, 0x69, 0x64, 0x64, 0x6c, 0x65, 0x77, 0x61, 0x72, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, - 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x08, 0x72, 0x65, 0x67, 0x69, 0x73, - 0x74, 0x65, 0x72, 0x12, 0x36, 0x0a, 0x08, 0x66, 0x65, 0x65, 0x64, 0x62, 0x61, 0x63, 0x6b, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, - 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x46, 0x65, 0x65, 0x64, 0x62, 0x61, 0x63, 0x6b, 0x48, - 0x00, 0x52, 0x08, 0x66, 0x65, 0x65, 0x64, 0x62, 0x61, 0x63, 0x6b, 0x42, 0x14, 0x0a, 0x12, 0x6d, - 0x69, 0x64, 0x64, 0x6c, 0x65, 0x77, 0x61, 0x72, 0x65, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, - 0x65, 0x22, 0xa6, 0x01, 0x0a, 0x16, 0x4d, 0x69, 0x64, 0x64, 0x6c, 0x65, 0x77, 0x61, 0x72, 0x65, - 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x27, 0x0a, 0x0f, - 0x6d, 0x69, 0x64, 0x64, 0x6c, 0x65, 0x77, 0x61, 0x72, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6d, 0x69, 0x64, 0x64, 0x6c, 0x65, 0x77, 0x61, 0x72, - 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x3d, 0x0a, 0x1b, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, - 0x6d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x5f, 0x63, 0x61, 0x76, 0x65, 0x61, 0x74, 0x5f, - 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x18, 0x63, 0x75, 0x73, 0x74, - 0x6f, 0x6d, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x43, 0x61, 0x76, 0x65, 0x61, 0x74, - 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x0e, 0x72, 0x65, 0x61, 0x64, 0x5f, 0x6f, 0x6e, 0x6c, - 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x72, 0x65, - 0x61, 0x64, 0x4f, 0x6e, 0x6c, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x22, 0x8b, 0x01, 0x0a, 0x11, 0x49, - 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x46, 0x65, 0x65, 0x64, 0x62, 0x61, 0x63, 0x6b, - 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, - 0x65, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x0f, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x35, 0x0a, 0x16, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x5f, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x15, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, - 0x72, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x2a, 0xcb, 0x02, 0x0a, 0x10, 0x4f, 0x75, 0x74, - 0x70, 0x75, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1b, 0x0a, - 0x17, 0x53, 0x43, 0x52, 0x49, 0x50, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x50, 0x55, 0x42, - 0x4b, 0x45, 0x59, 0x5f, 0x48, 0x41, 0x53, 0x48, 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x53, 0x43, - 0x52, 0x49, 0x50, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x43, 0x52, 0x49, 0x50, 0x54, - 0x5f, 0x48, 0x41, 0x53, 0x48, 0x10, 0x01, 0x12, 0x26, 0x0a, 0x22, 0x53, 0x43, 0x52, 0x49, 0x50, - 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x57, 0x49, 0x54, 0x4e, 0x45, 0x53, 0x53, 0x5f, 0x56, - 0x30, 0x5f, 0x50, 0x55, 0x42, 0x4b, 0x45, 0x59, 0x5f, 0x48, 0x41, 0x53, 0x48, 0x10, 0x02, 0x12, - 0x26, 0x0a, 0x22, 0x53, 0x43, 0x52, 0x49, 0x50, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x57, - 0x49, 0x54, 0x4e, 0x45, 0x53, 0x53, 0x5f, 0x56, 0x30, 0x5f, 0x53, 0x43, 0x52, 0x49, 0x50, 0x54, - 0x5f, 0x48, 0x41, 0x53, 0x48, 0x10, 0x03, 0x12, 0x16, 0x0a, 0x12, 0x53, 0x43, 0x52, 0x49, 0x50, - 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x50, 0x55, 0x42, 0x4b, 0x45, 0x59, 0x10, 0x04, 0x12, - 0x18, 0x0a, 0x14, 0x53, 0x43, 0x52, 0x49, 0x50, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4d, - 0x55, 0x4c, 0x54, 0x49, 0x53, 0x49, 0x47, 0x10, 0x05, 0x12, 0x18, 0x0a, 0x14, 0x53, 0x43, 0x52, - 0x49, 0x50, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4e, 0x55, 0x4c, 0x4c, 0x44, 0x41, 0x54, - 0x41, 0x10, 0x06, 0x12, 0x1c, 0x0a, 0x18, 0x53, 0x43, 0x52, 0x49, 0x50, 0x54, 0x5f, 0x54, 0x59, - 0x50, 0x45, 0x5f, 0x4e, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x4e, 0x44, 0x41, 0x52, 0x44, 0x10, - 0x07, 0x12, 0x1f, 0x0a, 0x1b, 0x53, 0x43, 0x52, 0x49, 0x50, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, - 0x5f, 0x57, 0x49, 0x54, 0x4e, 0x45, 0x53, 0x53, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, - 0x10, 0x08, 0x12, 0x22, 0x0a, 0x1e, 0x53, 0x43, 0x52, 0x49, 0x50, 0x54, 0x5f, 0x54, 0x59, 0x50, - 0x45, 0x5f, 0x57, 0x49, 0x54, 0x4e, 0x45, 0x53, 0x53, 0x5f, 0x56, 0x31, 0x5f, 0x54, 0x41, 0x50, - 0x52, 0x4f, 0x4f, 0x54, 0x10, 0x09, 0x2a, 0x62, 0x0a, 0x15, 0x43, 0x6f, 0x69, 0x6e, 0x53, 0x65, - 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, - 0x1e, 0x0a, 0x1a, 0x53, 0x54, 0x52, 0x41, 0x54, 0x45, 0x47, 0x59, 0x5f, 0x55, 0x53, 0x45, 0x5f, - 0x47, 0x4c, 0x4f, 0x42, 0x41, 0x4c, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x10, 0x00, 0x12, - 0x14, 0x0a, 0x10, 0x53, 0x54, 0x52, 0x41, 0x54, 0x45, 0x47, 0x59, 0x5f, 0x4c, 0x41, 0x52, 0x47, - 0x45, 0x53, 0x54, 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x54, 0x52, 0x41, 0x54, 0x45, 0x47, - 0x59, 0x5f, 0x52, 0x41, 0x4e, 0x44, 0x4f, 0x4d, 0x10, 0x02, 0x2a, 0xac, 0x01, 0x0a, 0x0b, 0x41, - 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x54, 0x79, 0x70, 0x65, 0x12, 0x17, 0x0a, 0x13, 0x57, 0x49, - 0x54, 0x4e, 0x45, 0x53, 0x53, 0x5f, 0x50, 0x55, 0x42, 0x4b, 0x45, 0x59, 0x5f, 0x48, 0x41, 0x53, - 0x48, 0x10, 0x00, 0x12, 0x16, 0x0a, 0x12, 0x4e, 0x45, 0x53, 0x54, 0x45, 0x44, 0x5f, 0x50, 0x55, - 0x42, 0x4b, 0x45, 0x59, 0x5f, 0x48, 0x41, 0x53, 0x48, 0x10, 0x01, 0x12, 0x1e, 0x0a, 0x1a, 0x55, - 0x4e, 0x55, 0x53, 0x45, 0x44, 0x5f, 0x57, 0x49, 0x54, 0x4e, 0x45, 0x53, 0x53, 0x5f, 0x50, 0x55, - 0x42, 0x4b, 0x45, 0x59, 0x5f, 0x48, 0x41, 0x53, 0x48, 0x10, 0x02, 0x12, 0x1d, 0x0a, 0x19, 0x55, - 0x4e, 0x55, 0x53, 0x45, 0x44, 0x5f, 0x4e, 0x45, 0x53, 0x54, 0x45, 0x44, 0x5f, 0x50, 0x55, 0x42, - 0x4b, 0x45, 0x59, 0x5f, 0x48, 0x41, 0x53, 0x48, 0x10, 0x03, 0x12, 0x12, 0x0a, 0x0e, 0x54, 0x41, - 0x50, 0x52, 0x4f, 0x4f, 0x54, 0x5f, 0x50, 0x55, 0x42, 0x4b, 0x45, 0x59, 0x10, 0x04, 0x12, 0x19, - 0x0a, 0x15, 0x55, 0x4e, 0x55, 0x53, 0x45, 0x44, 0x5f, 0x54, 0x41, 0x50, 0x52, 0x4f, 0x4f, 0x54, - 0x5f, 0x50, 0x55, 0x42, 0x4b, 0x45, 0x59, 0x10, 0x05, 0x2a, 0xa8, 0x01, 0x0a, 0x0e, 0x43, 0x6f, - 0x6d, 0x6d, 0x69, 0x74, 0x6d, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1b, 0x0a, 0x17, - 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x5f, 0x43, 0x4f, 0x4d, 0x4d, 0x49, 0x54, 0x4d, 0x45, - 0x4e, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x4c, 0x45, 0x47, - 0x41, 0x43, 0x59, 0x10, 0x01, 0x12, 0x15, 0x0a, 0x11, 0x53, 0x54, 0x41, 0x54, 0x49, 0x43, 0x5f, - 0x52, 0x45, 0x4d, 0x4f, 0x54, 0x45, 0x5f, 0x4b, 0x45, 0x59, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, - 0x41, 0x4e, 0x43, 0x48, 0x4f, 0x52, 0x53, 0x10, 0x03, 0x12, 0x19, 0x0a, 0x15, 0x53, 0x43, 0x52, - 0x49, 0x50, 0x54, 0x5f, 0x45, 0x4e, 0x46, 0x4f, 0x52, 0x43, 0x45, 0x44, 0x5f, 0x4c, 0x45, 0x41, - 0x53, 0x45, 0x10, 0x04, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x49, 0x4d, 0x50, 0x4c, 0x45, 0x5f, 0x54, - 0x41, 0x50, 0x52, 0x4f, 0x4f, 0x54, 0x10, 0x05, 0x12, 0x1a, 0x0a, 0x16, 0x53, 0x49, 0x4d, 0x50, - 0x4c, 0x45, 0x5f, 0x54, 0x41, 0x50, 0x52, 0x4f, 0x4f, 0x54, 0x5f, 0x4f, 0x56, 0x45, 0x52, 0x4c, - 0x41, 0x59, 0x10, 0x06, 0x2a, 0x61, 0x0a, 0x09, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x6f, - 0x72, 0x12, 0x15, 0x0a, 0x11, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41, 0x54, 0x4f, 0x52, 0x5f, 0x55, - 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x13, 0x0a, 0x0f, 0x49, 0x4e, 0x49, 0x54, - 0x49, 0x41, 0x54, 0x4f, 0x52, 0x5f, 0x4c, 0x4f, 0x43, 0x41, 0x4c, 0x10, 0x01, 0x12, 0x14, 0x0a, - 0x10, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41, 0x54, 0x4f, 0x52, 0x5f, 0x52, 0x45, 0x4d, 0x4f, 0x54, - 0x45, 0x10, 0x02, 0x12, 0x12, 0x0a, 0x0e, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41, 0x54, 0x4f, 0x52, - 0x5f, 0x42, 0x4f, 0x54, 0x48, 0x10, 0x03, 0x2a, 0x60, 0x0a, 0x0e, 0x52, 0x65, 0x73, 0x6f, 0x6c, - 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x10, 0x0a, 0x0c, 0x54, 0x59, 0x50, - 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x41, - 0x4e, 0x43, 0x48, 0x4f, 0x52, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, 0x49, 0x4e, 0x43, 0x4f, 0x4d, - 0x49, 0x4e, 0x47, 0x5f, 0x48, 0x54, 0x4c, 0x43, 0x10, 0x02, 0x12, 0x11, 0x0a, 0x0d, 0x4f, 0x55, - 0x54, 0x47, 0x4f, 0x49, 0x4e, 0x47, 0x5f, 0x48, 0x54, 0x4c, 0x43, 0x10, 0x03, 0x12, 0x0a, 0x0a, - 0x06, 0x43, 0x4f, 0x4d, 0x4d, 0x49, 0x54, 0x10, 0x04, 0x2a, 0x71, 0x0a, 0x11, 0x52, 0x65, 0x73, - 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x4f, 0x75, 0x74, 0x63, 0x6f, 0x6d, 0x65, 0x12, 0x13, - 0x0a, 0x0f, 0x4f, 0x55, 0x54, 0x43, 0x4f, 0x4d, 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, - 0x4e, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x43, 0x4c, 0x41, 0x49, 0x4d, 0x45, 0x44, 0x10, 0x01, - 0x12, 0x0d, 0x0a, 0x09, 0x55, 0x4e, 0x43, 0x4c, 0x41, 0x49, 0x4d, 0x45, 0x44, 0x10, 0x02, 0x12, - 0x0d, 0x0a, 0x09, 0x41, 0x42, 0x41, 0x4e, 0x44, 0x4f, 0x4e, 0x45, 0x44, 0x10, 0x03, 0x12, 0x0f, - 0x0a, 0x0b, 0x46, 0x49, 0x52, 0x53, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x47, 0x45, 0x10, 0x04, 0x12, - 0x0b, 0x0a, 0x07, 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x10, 0x05, 0x2a, 0x39, 0x0a, 0x0e, - 0x4e, 0x6f, 0x64, 0x65, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, - 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x1a, 0x0a, 0x16, 0x42, - 0x45, 0x54, 0x57, 0x45, 0x45, 0x4e, 0x4e, 0x45, 0x53, 0x53, 0x5f, 0x43, 0x45, 0x4e, 0x54, 0x52, - 0x41, 0x4c, 0x49, 0x54, 0x59, 0x10, 0x01, 0x2a, 0x3b, 0x0a, 0x10, 0x49, 0x6e, 0x76, 0x6f, 0x69, - 0x63, 0x65, 0x48, 0x54, 0x4c, 0x43, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0c, 0x0a, 0x08, 0x41, - 0x43, 0x43, 0x45, 0x50, 0x54, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x45, 0x54, - 0x54, 0x4c, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, - 0x45, 0x44, 0x10, 0x02, 0x2a, 0xf6, 0x01, 0x0a, 0x14, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, - 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x17, 0x0a, - 0x13, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, - 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x1a, 0x0a, 0x16, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, - 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, - 0x10, 0x01, 0x12, 0x1b, 0x0a, 0x17, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, - 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x4e, 0x4f, 0x5f, 0x52, 0x4f, 0x55, 0x54, 0x45, 0x10, 0x02, 0x12, - 0x18, 0x0a, 0x14, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, - 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x12, 0x2c, 0x0a, 0x28, 0x46, 0x41, 0x49, - 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x49, 0x4e, 0x43, 0x4f, - 0x52, 0x52, 0x45, 0x43, 0x54, 0x5f, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x44, 0x45, - 0x54, 0x41, 0x49, 0x4c, 0x53, 0x10, 0x04, 0x12, 0x27, 0x0a, 0x23, 0x46, 0x41, 0x49, 0x4c, 0x55, - 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x49, 0x4e, 0x53, 0x55, 0x46, 0x46, - 0x49, 0x43, 0x49, 0x45, 0x4e, 0x54, 0x5f, 0x42, 0x41, 0x4c, 0x41, 0x4e, 0x43, 0x45, 0x10, 0x05, - 0x12, 0x1b, 0x0a, 0x17, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, - 0x4f, 0x4e, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x45, 0x44, 0x10, 0x06, 0x2a, 0x89, 0x05, - 0x0a, 0x0a, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x42, 0x69, 0x74, 0x12, 0x18, 0x0a, 0x14, - 0x44, 0x41, 0x54, 0x41, 0x4c, 0x4f, 0x53, 0x53, 0x5f, 0x50, 0x52, 0x4f, 0x54, 0x45, 0x43, 0x54, - 0x5f, 0x52, 0x45, 0x51, 0x10, 0x00, 0x12, 0x18, 0x0a, 0x14, 0x44, 0x41, 0x54, 0x41, 0x4c, 0x4f, - 0x53, 0x53, 0x5f, 0x50, 0x52, 0x4f, 0x54, 0x45, 0x43, 0x54, 0x5f, 0x4f, 0x50, 0x54, 0x10, 0x01, - 0x12, 0x17, 0x0a, 0x13, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41, 0x4c, 0x5f, 0x52, 0x4f, 0x55, 0x49, - 0x4e, 0x47, 0x5f, 0x53, 0x59, 0x4e, 0x43, 0x10, 0x03, 0x12, 0x1f, 0x0a, 0x1b, 0x55, 0x50, 0x46, - 0x52, 0x4f, 0x4e, 0x54, 0x5f, 0x53, 0x48, 0x55, 0x54, 0x44, 0x4f, 0x57, 0x4e, 0x5f, 0x53, 0x43, - 0x52, 0x49, 0x50, 0x54, 0x5f, 0x52, 0x45, 0x51, 0x10, 0x04, 0x12, 0x1f, 0x0a, 0x1b, 0x55, 0x50, - 0x46, 0x52, 0x4f, 0x4e, 0x54, 0x5f, 0x53, 0x48, 0x55, 0x54, 0x44, 0x4f, 0x57, 0x4e, 0x5f, 0x53, - 0x43, 0x52, 0x49, 0x50, 0x54, 0x5f, 0x4f, 0x50, 0x54, 0x10, 0x05, 0x12, 0x16, 0x0a, 0x12, 0x47, - 0x4f, 0x53, 0x53, 0x49, 0x50, 0x5f, 0x51, 0x55, 0x45, 0x52, 0x49, 0x45, 0x53, 0x5f, 0x52, 0x45, - 0x51, 0x10, 0x06, 0x12, 0x16, 0x0a, 0x12, 0x47, 0x4f, 0x53, 0x53, 0x49, 0x50, 0x5f, 0x51, 0x55, - 0x45, 0x52, 0x49, 0x45, 0x53, 0x5f, 0x4f, 0x50, 0x54, 0x10, 0x07, 0x12, 0x11, 0x0a, 0x0d, 0x54, - 0x4c, 0x56, 0x5f, 0x4f, 0x4e, 0x49, 0x4f, 0x4e, 0x5f, 0x52, 0x45, 0x51, 0x10, 0x08, 0x12, 0x11, - 0x0a, 0x0d, 0x54, 0x4c, 0x56, 0x5f, 0x4f, 0x4e, 0x49, 0x4f, 0x4e, 0x5f, 0x4f, 0x50, 0x54, 0x10, - 0x09, 0x12, 0x1a, 0x0a, 0x16, 0x45, 0x58, 0x54, 0x5f, 0x47, 0x4f, 0x53, 0x53, 0x49, 0x50, 0x5f, - 0x51, 0x55, 0x45, 0x52, 0x49, 0x45, 0x53, 0x5f, 0x52, 0x45, 0x51, 0x10, 0x0a, 0x12, 0x1a, 0x0a, - 0x16, 0x45, 0x58, 0x54, 0x5f, 0x47, 0x4f, 0x53, 0x53, 0x49, 0x50, 0x5f, 0x51, 0x55, 0x45, 0x52, - 0x49, 0x45, 0x53, 0x5f, 0x4f, 0x50, 0x54, 0x10, 0x0b, 0x12, 0x19, 0x0a, 0x15, 0x53, 0x54, 0x41, - 0x54, 0x49, 0x43, 0x5f, 0x52, 0x45, 0x4d, 0x4f, 0x54, 0x45, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x52, - 0x45, 0x51, 0x10, 0x0c, 0x12, 0x19, 0x0a, 0x15, 0x53, 0x54, 0x41, 0x54, 0x49, 0x43, 0x5f, 0x52, - 0x45, 0x4d, 0x4f, 0x54, 0x45, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x4f, 0x50, 0x54, 0x10, 0x0d, 0x12, - 0x14, 0x0a, 0x10, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x41, 0x44, 0x44, 0x52, 0x5f, - 0x52, 0x45, 0x51, 0x10, 0x0e, 0x12, 0x14, 0x0a, 0x10, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, - 0x5f, 0x41, 0x44, 0x44, 0x52, 0x5f, 0x4f, 0x50, 0x54, 0x10, 0x0f, 0x12, 0x0b, 0x0a, 0x07, 0x4d, - 0x50, 0x50, 0x5f, 0x52, 0x45, 0x51, 0x10, 0x10, 0x12, 0x0b, 0x0a, 0x07, 0x4d, 0x50, 0x50, 0x5f, - 0x4f, 0x50, 0x54, 0x10, 0x11, 0x12, 0x16, 0x0a, 0x12, 0x57, 0x55, 0x4d, 0x42, 0x4f, 0x5f, 0x43, - 0x48, 0x41, 0x4e, 0x4e, 0x45, 0x4c, 0x53, 0x5f, 0x52, 0x45, 0x51, 0x10, 0x12, 0x12, 0x16, 0x0a, - 0x12, 0x57, 0x55, 0x4d, 0x42, 0x4f, 0x5f, 0x43, 0x48, 0x41, 0x4e, 0x4e, 0x45, 0x4c, 0x53, 0x5f, - 0x4f, 0x50, 0x54, 0x10, 0x13, 0x12, 0x0f, 0x0a, 0x0b, 0x41, 0x4e, 0x43, 0x48, 0x4f, 0x52, 0x53, - 0x5f, 0x52, 0x45, 0x51, 0x10, 0x14, 0x12, 0x0f, 0x0a, 0x0b, 0x41, 0x4e, 0x43, 0x48, 0x4f, 0x52, - 0x53, 0x5f, 0x4f, 0x50, 0x54, 0x10, 0x15, 0x12, 0x1d, 0x0a, 0x19, 0x41, 0x4e, 0x43, 0x48, 0x4f, - 0x52, 0x53, 0x5f, 0x5a, 0x45, 0x52, 0x4f, 0x5f, 0x46, 0x45, 0x45, 0x5f, 0x48, 0x54, 0x4c, 0x43, - 0x5f, 0x52, 0x45, 0x51, 0x10, 0x16, 0x12, 0x1d, 0x0a, 0x19, 0x41, 0x4e, 0x43, 0x48, 0x4f, 0x52, - 0x53, 0x5f, 0x5a, 0x45, 0x52, 0x4f, 0x5f, 0x46, 0x45, 0x45, 0x5f, 0x48, 0x54, 0x4c, 0x43, 0x5f, - 0x4f, 0x50, 0x54, 0x10, 0x17, 0x12, 0x1b, 0x0a, 0x17, 0x52, 0x4f, 0x55, 0x54, 0x45, 0x5f, 0x42, - 0x4c, 0x49, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x5f, 0x52, 0x45, 0x51, 0x55, 0x49, 0x52, 0x45, 0x44, - 0x10, 0x18, 0x12, 0x1b, 0x0a, 0x17, 0x52, 0x4f, 0x55, 0x54, 0x45, 0x5f, 0x42, 0x4c, 0x49, 0x4e, - 0x44, 0x49, 0x4e, 0x47, 0x5f, 0x4f, 0x50, 0x54, 0x49, 0x4f, 0x4e, 0x41, 0x4c, 0x10, 0x19, 0x12, - 0x0b, 0x0a, 0x07, 0x41, 0x4d, 0x50, 0x5f, 0x52, 0x45, 0x51, 0x10, 0x1e, 0x12, 0x0b, 0x0a, 0x07, - 0x41, 0x4d, 0x50, 0x5f, 0x4f, 0x50, 0x54, 0x10, 0x1f, 0x2a, 0xac, 0x01, 0x0a, 0x0d, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x12, 0x1a, 0x0a, 0x16, 0x55, - 0x50, 0x44, 0x41, 0x54, 0x45, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x55, 0x4e, - 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x1a, 0x0a, 0x16, 0x55, 0x50, 0x44, 0x41, 0x54, - 0x45, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, - 0x47, 0x10, 0x01, 0x12, 0x1c, 0x0a, 0x18, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x5f, 0x46, 0x41, - 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x46, 0x4f, 0x55, 0x4e, 0x44, 0x10, - 0x02, 0x12, 0x1f, 0x0a, 0x1b, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x5f, 0x46, 0x41, 0x49, 0x4c, - 0x55, 0x52, 0x45, 0x5f, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x5f, 0x45, 0x52, 0x52, - 0x10, 0x03, 0x12, 0x24, 0x0a, 0x20, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x5f, 0x46, 0x41, 0x49, - 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x5f, 0x50, 0x41, 0x52, - 0x41, 0x4d, 0x45, 0x54, 0x45, 0x52, 0x10, 0x04, 0x32, 0xc5, 0x29, 0x0a, 0x09, 0x4c, 0x69, 0x67, - 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x12, 0x4a, 0x0a, 0x0d, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, - 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x1b, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, - 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x57, 0x61, 0x6c, - 0x6c, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x4d, 0x0a, 0x0e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x42, 0x61, 0x6c, - 0x61, 0x6e, 0x63, 0x65, 0x12, 0x1c, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, - 0x6e, 0x6e, 0x65, 0x6c, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, - 0x65, 0x6c, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x4b, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1d, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x47, 0x65, 0x74, - 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x72, 0x61, 0x6e, - 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x44, - 0x0a, 0x0b, 0x45, 0x73, 0x74, 0x69, 0x6d, 0x61, 0x74, 0x65, 0x46, 0x65, 0x65, 0x12, 0x19, 0x2e, - 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x45, 0x73, 0x74, 0x69, 0x6d, 0x61, 0x74, 0x65, 0x46, 0x65, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, - 0x2e, 0x45, 0x73, 0x74, 0x69, 0x6d, 0x61, 0x74, 0x65, 0x46, 0x65, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x09, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f, 0x69, 0x6e, - 0x73, 0x12, 0x17, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f, - 0x69, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x6c, 0x6e, 0x72, - 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f, 0x69, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x6e, 0x73, 0x70, - 0x65, 0x6e, 0x74, 0x12, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, - 0x55, 0x6e, 0x73, 0x70, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, - 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x6e, 0x73, 0x70, 0x65, - 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x15, 0x53, 0x75, - 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x12, 0x1d, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x47, 0x65, 0x74, 0x54, - 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, - 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x30, 0x01, 0x12, 0x3b, 0x0a, 0x08, 0x53, 0x65, 0x6e, 0x64, - 0x4d, 0x61, 0x6e, 0x79, 0x12, 0x16, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, - 0x64, 0x4d, 0x61, 0x6e, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x6c, - 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x4d, 0x61, 0x6e, 0x79, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x0a, 0x4e, 0x65, 0x77, 0x41, 0x64, 0x64, 0x72, - 0x65, 0x73, 0x73, 0x12, 0x18, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4e, 0x65, 0x77, 0x41, - 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, - 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4e, 0x65, 0x77, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x0b, 0x53, 0x69, 0x67, 0x6e, - 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, - 0x53, 0x69, 0x67, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x4d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4a, - 0x0a, 0x0d, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, - 0x1b, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x4d, 0x65, - 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x6c, - 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x4d, 0x65, 0x73, 0x73, 0x61, - 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x0b, 0x43, 0x6f, - 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x50, 0x65, 0x65, 0x72, 0x12, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, - 0x63, 0x2e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6f, 0x6e, - 0x6e, 0x65, 0x63, 0x74, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x4d, 0x0a, 0x0e, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x50, 0x65, - 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x44, 0x69, 0x73, 0x63, 0x6f, - 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x1d, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x6e, 0x6e, - 0x65, 0x63, 0x74, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x3e, 0x0a, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x17, 0x2e, 0x6c, - 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x65, 0x65, 0x72, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, - 0x73, 0x74, 0x50, 0x65, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x47, 0x0a, 0x13, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x50, 0x65, 0x65, 0x72, - 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x1c, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, - 0x65, 0x65, 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x10, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x65, 0x65, - 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x12, 0x38, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x49, - 0x6e, 0x66, 0x6f, 0x12, 0x15, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x47, 0x65, 0x74, 0x49, - 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x6c, 0x6e, 0x72, - 0x70, 0x63, 0x2e, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x47, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x44, 0x65, 0x62, 0x75, 0x67, 0x49, 0x6e, - 0x66, 0x6f, 0x12, 0x1a, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, - 0x62, 0x75, 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, - 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x62, 0x75, 0x67, 0x49, - 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x50, 0x0a, 0x0f, 0x47, - 0x65, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1d, - 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x76, 0x65, - 0x72, 0x79, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, - 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, - 0x79, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x50, 0x0a, - 0x0f, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, - 0x12, 0x1d, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, - 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x1e, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x43, - 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x47, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x12, - 0x1a, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x68, 0x61, 0x6e, - 0x6e, 0x65, 0x6c, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x6c, 0x6e, - 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x56, 0x0a, 0x16, 0x53, 0x75, 0x62, 0x73, - 0x63, 0x72, 0x69, 0x62, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x45, 0x76, 0x65, 0x6e, - 0x74, 0x73, 0x12, 0x1f, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, - 0x65, 0x6c, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x4c, 0x69, 0x73, 0x74, 0x12, 0x3b, 0x0a, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, + 0x63, 0x2e, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x73, 0x22, 0x18, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xe4, 0x01, 0x0a, 0x17, + 0x4c, 0x69, 0x73, 0x74, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x64, 0x0a, 0x12, 0x6d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x5f, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x11, 0x6d, 0x65, 0x74, 0x68, + 0x6f, 0x64, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x1a, 0x63, 0x0a, + 0x16, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x33, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, + 0x2e, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, + 0x38, 0x01, 0x22, 0xcc, 0x08, 0x0a, 0x07, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x12, 0x2e, + 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x6c, + 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x2e, 0x46, 0x61, 0x69, + 0x6c, 0x75, 0x72, 0x65, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x3b, + 0x0a, 0x0e, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, + 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x0d, 0x63, 0x68, + 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x68, + 0x74, 0x6c, 0x63, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, + 0x68, 0x74, 0x6c, 0x63, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x22, 0x0a, 0x0d, 0x6f, 0x6e, 0x69, 0x6f, + 0x6e, 0x5f, 0x73, 0x68, 0x61, 0x5f, 0x32, 0x35, 0x36, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x0b, 0x6f, 0x6e, 0x69, 0x6f, 0x6e, 0x53, 0x68, 0x61, 0x32, 0x35, 0x36, 0x12, 0x1f, 0x0a, 0x0b, + 0x63, 0x6c, 0x74, 0x76, 0x5f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x0a, 0x63, 0x6c, 0x74, 0x76, 0x45, 0x78, 0x70, 0x69, 0x72, 0x79, 0x12, 0x14, 0x0a, + 0x05, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x66, 0x6c, + 0x61, 0x67, 0x73, 0x12, 0x30, 0x0a, 0x14, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x5f, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x08, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x12, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, + 0x09, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x22, 0x8b, 0x06, + 0x0a, 0x0b, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x0c, 0x0a, + 0x08, 0x52, 0x45, 0x53, 0x45, 0x52, 0x56, 0x45, 0x44, 0x10, 0x00, 0x12, 0x28, 0x0a, 0x24, 0x49, + 0x4e, 0x43, 0x4f, 0x52, 0x52, 0x45, 0x43, 0x54, 0x5f, 0x4f, 0x52, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, + 0x4f, 0x57, 0x4e, 0x5f, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x44, 0x45, 0x54, 0x41, + 0x49, 0x4c, 0x53, 0x10, 0x01, 0x12, 0x1c, 0x0a, 0x18, 0x49, 0x4e, 0x43, 0x4f, 0x52, 0x52, 0x45, + 0x43, 0x54, 0x5f, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x41, 0x4d, 0x4f, 0x55, 0x4e, + 0x54, 0x10, 0x02, 0x12, 0x1f, 0x0a, 0x1b, 0x46, 0x49, 0x4e, 0x41, 0x4c, 0x5f, 0x49, 0x4e, 0x43, + 0x4f, 0x52, 0x52, 0x45, 0x43, 0x54, 0x5f, 0x43, 0x4c, 0x54, 0x56, 0x5f, 0x45, 0x58, 0x50, 0x49, + 0x52, 0x59, 0x10, 0x03, 0x12, 0x1f, 0x0a, 0x1b, 0x46, 0x49, 0x4e, 0x41, 0x4c, 0x5f, 0x49, 0x4e, + 0x43, 0x4f, 0x52, 0x52, 0x45, 0x43, 0x54, 0x5f, 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x41, 0x4d, 0x4f, + 0x55, 0x4e, 0x54, 0x10, 0x04, 0x12, 0x19, 0x0a, 0x15, 0x46, 0x49, 0x4e, 0x41, 0x4c, 0x5f, 0x45, + 0x58, 0x50, 0x49, 0x52, 0x59, 0x5f, 0x54, 0x4f, 0x4f, 0x5f, 0x53, 0x4f, 0x4f, 0x4e, 0x10, 0x05, + 0x12, 0x11, 0x0a, 0x0d, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x5f, 0x52, 0x45, 0x41, 0x4c, + 0x4d, 0x10, 0x06, 0x12, 0x13, 0x0a, 0x0f, 0x45, 0x58, 0x50, 0x49, 0x52, 0x59, 0x5f, 0x54, 0x4f, + 0x4f, 0x5f, 0x53, 0x4f, 0x4f, 0x4e, 0x10, 0x07, 0x12, 0x19, 0x0a, 0x15, 0x49, 0x4e, 0x56, 0x41, + 0x4c, 0x49, 0x44, 0x5f, 0x4f, 0x4e, 0x49, 0x4f, 0x4e, 0x5f, 0x56, 0x45, 0x52, 0x53, 0x49, 0x4f, + 0x4e, 0x10, 0x08, 0x12, 0x16, 0x0a, 0x12, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x5f, 0x4f, + 0x4e, 0x49, 0x4f, 0x4e, 0x5f, 0x48, 0x4d, 0x41, 0x43, 0x10, 0x09, 0x12, 0x15, 0x0a, 0x11, 0x49, + 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x5f, 0x4f, 0x4e, 0x49, 0x4f, 0x4e, 0x5f, 0x4b, 0x45, 0x59, + 0x10, 0x0a, 0x12, 0x18, 0x0a, 0x14, 0x41, 0x4d, 0x4f, 0x55, 0x4e, 0x54, 0x5f, 0x42, 0x45, 0x4c, + 0x4f, 0x57, 0x5f, 0x4d, 0x49, 0x4e, 0x49, 0x4d, 0x55, 0x4d, 0x10, 0x0b, 0x12, 0x14, 0x0a, 0x10, + 0x46, 0x45, 0x45, 0x5f, 0x49, 0x4e, 0x53, 0x55, 0x46, 0x46, 0x49, 0x43, 0x49, 0x45, 0x4e, 0x54, + 0x10, 0x0c, 0x12, 0x19, 0x0a, 0x15, 0x49, 0x4e, 0x43, 0x4f, 0x52, 0x52, 0x45, 0x43, 0x54, 0x5f, + 0x43, 0x4c, 0x54, 0x56, 0x5f, 0x45, 0x58, 0x50, 0x49, 0x52, 0x59, 0x10, 0x0d, 0x12, 0x14, 0x0a, + 0x10, 0x43, 0x48, 0x41, 0x4e, 0x4e, 0x45, 0x4c, 0x5f, 0x44, 0x49, 0x53, 0x41, 0x42, 0x4c, 0x45, + 0x44, 0x10, 0x0e, 0x12, 0x1d, 0x0a, 0x19, 0x54, 0x45, 0x4d, 0x50, 0x4f, 0x52, 0x41, 0x52, 0x59, + 0x5f, 0x43, 0x48, 0x41, 0x4e, 0x4e, 0x45, 0x4c, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, + 0x10, 0x0f, 0x12, 0x21, 0x0a, 0x1d, 0x52, 0x45, 0x51, 0x55, 0x49, 0x52, 0x45, 0x44, 0x5f, 0x4e, + 0x4f, 0x44, 0x45, 0x5f, 0x46, 0x45, 0x41, 0x54, 0x55, 0x52, 0x45, 0x5f, 0x4d, 0x49, 0x53, 0x53, + 0x49, 0x4e, 0x47, 0x10, 0x10, 0x12, 0x24, 0x0a, 0x20, 0x52, 0x45, 0x51, 0x55, 0x49, 0x52, 0x45, + 0x44, 0x5f, 0x43, 0x48, 0x41, 0x4e, 0x4e, 0x45, 0x4c, 0x5f, 0x46, 0x45, 0x41, 0x54, 0x55, 0x52, + 0x45, 0x5f, 0x4d, 0x49, 0x53, 0x53, 0x49, 0x4e, 0x47, 0x10, 0x11, 0x12, 0x15, 0x0a, 0x11, 0x55, + 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x5f, 0x4e, 0x45, 0x58, 0x54, 0x5f, 0x50, 0x45, 0x45, 0x52, + 0x10, 0x12, 0x12, 0x1a, 0x0a, 0x16, 0x54, 0x45, 0x4d, 0x50, 0x4f, 0x52, 0x41, 0x52, 0x59, 0x5f, + 0x4e, 0x4f, 0x44, 0x45, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x10, 0x13, 0x12, 0x1a, + 0x0a, 0x16, 0x50, 0x45, 0x52, 0x4d, 0x41, 0x4e, 0x45, 0x4e, 0x54, 0x5f, 0x4e, 0x4f, 0x44, 0x45, + 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x10, 0x14, 0x12, 0x1d, 0x0a, 0x19, 0x50, 0x45, + 0x52, 0x4d, 0x41, 0x4e, 0x45, 0x4e, 0x54, 0x5f, 0x43, 0x48, 0x41, 0x4e, 0x4e, 0x45, 0x4c, 0x5f, + 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x10, 0x15, 0x12, 0x12, 0x0a, 0x0e, 0x45, 0x58, 0x50, + 0x49, 0x52, 0x59, 0x5f, 0x54, 0x4f, 0x4f, 0x5f, 0x46, 0x41, 0x52, 0x10, 0x16, 0x12, 0x0f, 0x0a, + 0x0b, 0x4d, 0x50, 0x50, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x10, 0x17, 0x12, 0x19, + 0x0a, 0x15, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x5f, 0x4f, 0x4e, 0x49, 0x4f, 0x4e, 0x5f, + 0x50, 0x41, 0x59, 0x4c, 0x4f, 0x41, 0x44, 0x10, 0x18, 0x12, 0x1a, 0x0a, 0x16, 0x49, 0x4e, 0x56, + 0x41, 0x4c, 0x49, 0x44, 0x5f, 0x4f, 0x4e, 0x49, 0x4f, 0x4e, 0x5f, 0x42, 0x4c, 0x49, 0x4e, 0x44, + 0x49, 0x4e, 0x47, 0x10, 0x19, 0x12, 0x15, 0x0a, 0x10, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, + 0x4c, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x10, 0xe5, 0x07, 0x12, 0x14, 0x0a, 0x0f, + 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x10, + 0xe6, 0x07, 0x12, 0x17, 0x0a, 0x12, 0x55, 0x4e, 0x52, 0x45, 0x41, 0x44, 0x41, 0x42, 0x4c, 0x45, + 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x10, 0xe7, 0x07, 0x4a, 0x04, 0x08, 0x02, 0x10, + 0x03, 0x22, 0xb3, 0x03, 0x0a, 0x0d, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, + 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x48, 0x61, 0x73, 0x68, + 0x12, 0x1b, 0x0a, 0x07, 0x63, 0x68, 0x61, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x04, 0x42, 0x02, 0x30, 0x01, 0x52, 0x06, 0x63, 0x68, 0x61, 0x6e, 0x49, 0x64, 0x12, 0x1c, 0x0a, + 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, + 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x23, 0x0a, 0x0d, 0x6d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18, 0x0a, 0x20, 0x01, + 0x28, 0x0d, 0x52, 0x0c, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x46, 0x6c, 0x61, 0x67, 0x73, + 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x66, 0x6c, 0x61, 0x67, + 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0c, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, + 0x46, 0x6c, 0x61, 0x67, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x6c, 0x6f, + 0x63, 0x6b, 0x5f, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0d, + 0x74, 0x69, 0x6d, 0x65, 0x4c, 0x6f, 0x63, 0x6b, 0x44, 0x65, 0x6c, 0x74, 0x61, 0x12, 0x2a, 0x0a, + 0x11, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x6d, 0x69, 0x6e, 0x69, 0x6d, 0x75, 0x6d, 0x5f, 0x6d, 0x73, + 0x61, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0f, 0x68, 0x74, 0x6c, 0x63, 0x4d, 0x69, + 0x6e, 0x69, 0x6d, 0x75, 0x6d, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x61, 0x73, + 0x65, 0x5f, 0x66, 0x65, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x62, 0x61, 0x73, + 0x65, 0x46, 0x65, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x66, 0x65, 0x65, 0x5f, 0x72, 0x61, 0x74, 0x65, + 0x18, 0x09, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x66, 0x65, 0x65, 0x52, 0x61, 0x74, 0x65, 0x12, + 0x2a, 0x0a, 0x11, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x6d, 0x61, 0x78, 0x69, 0x6d, 0x75, 0x6d, 0x5f, + 0x6d, 0x73, 0x61, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0f, 0x68, 0x74, 0x6c, 0x63, + 0x4d, 0x61, 0x78, 0x69, 0x6d, 0x75, 0x6d, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x2a, 0x0a, 0x11, 0x65, + 0x78, 0x74, 0x72, 0x61, 0x5f, 0x6f, 0x70, 0x61, 0x71, 0x75, 0x65, 0x5f, 0x64, 0x61, 0x74, 0x61, + 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x65, 0x78, 0x74, 0x72, 0x61, 0x4f, 0x70, 0x61, + 0x71, 0x75, 0x65, 0x44, 0x61, 0x74, 0x61, 0x22, 0x5d, 0x0a, 0x0a, 0x4d, 0x61, 0x63, 0x61, 0x72, + 0x6f, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, + 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, + 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x03, 0x6f, 0x70, 0x73, + 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x09, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4f, + 0x70, 0x52, 0x03, 0x6f, 0x70, 0x73, 0x22, 0x36, 0x0a, 0x02, 0x4f, 0x70, 0x12, 0x16, 0x0a, 0x06, + 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x65, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, + 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xdd, + 0x01, 0x0a, 0x13, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4d, 0x61, 0x63, 0x50, 0x65, 0x72, 0x6d, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x6d, 0x61, 0x63, 0x61, 0x72, 0x6f, + 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x6d, 0x61, 0x63, 0x61, 0x72, 0x6f, + 0x6f, 0x6e, 0x12, 0x3b, 0x0a, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, + 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x52, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, + 0x1e, 0x0a, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, + 0x4d, 0x0a, 0x24, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x5f, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, + 0x5f, 0x70, 0x65, 0x72, 0x6d, 0x73, 0x5f, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x66, 0x75, 0x6c, 0x6c, + 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1f, 0x63, + 0x68, 0x65, 0x63, 0x6b, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x50, 0x65, 0x72, 0x6d, 0x73, + 0x46, 0x72, 0x6f, 0x6d, 0x46, 0x75, 0x6c, 0x6c, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x22, 0x2c, + 0x0a, 0x14, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4d, 0x61, 0x63, 0x50, 0x65, 0x72, 0x6d, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x22, 0xa4, 0x04, 0x0a, + 0x14, 0x52, 0x50, 0x43, 0x4d, 0x69, 0x64, 0x64, 0x6c, 0x65, 0x77, 0x61, 0x72, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x72, 0x61, 0x77, 0x5f, 0x6d, 0x61, 0x63, 0x61, + 0x72, 0x6f, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x72, 0x61, 0x77, 0x4d, + 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x12, 0x36, 0x0a, 0x17, 0x63, 0x75, 0x73, 0x74, 0x6f, + 0x6d, 0x5f, 0x63, 0x61, 0x76, 0x65, 0x61, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, + 0x43, 0x61, 0x76, 0x65, 0x61, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, + 0x34, 0x0a, 0x0b, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x74, 0x72, + 0x65, 0x61, 0x6d, 0x41, 0x75, 0x74, 0x68, 0x48, 0x00, 0x52, 0x0a, 0x73, 0x74, 0x72, 0x65, 0x61, + 0x6d, 0x41, 0x75, 0x74, 0x68, 0x12, 0x2d, 0x0a, 0x07, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x52, + 0x50, 0x43, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x48, 0x00, 0x52, 0x07, 0x72, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x52, + 0x50, 0x43, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x48, 0x00, 0x52, 0x08, 0x72, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x23, 0x0a, 0x0c, 0x72, 0x65, 0x67, 0x5f, 0x63, 0x6f, 0x6d, + 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0b, 0x72, + 0x65, 0x67, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x15, 0x0a, 0x06, 0x6d, 0x73, + 0x67, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x6d, 0x73, 0x67, 0x49, + 0x64, 0x12, 0x55, 0x0a, 0x0e, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x70, 0x61, + 0x69, 0x72, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x6c, 0x6e, 0x72, 0x70, + 0x63, 0x2e, 0x52, 0x50, 0x43, 0x4d, 0x69, 0x64, 0x64, 0x6c, 0x65, 0x77, 0x61, 0x72, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x50, + 0x61, 0x69, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0d, 0x6d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x50, 0x61, 0x69, 0x72, 0x73, 0x1a, 0x57, 0x0a, 0x12, 0x4d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x50, 0x61, 0x69, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, + 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, + 0x12, 0x2b, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x15, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, + 0x01, 0x42, 0x10, 0x0a, 0x0e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x5f, 0x74, + 0x79, 0x70, 0x65, 0x22, 0x28, 0x0a, 0x0e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x22, 0x34, 0x0a, + 0x0a, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x41, 0x75, 0x74, 0x68, 0x12, 0x26, 0x0a, 0x0f, 0x6d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x66, 0x75, 0x6c, 0x6c, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x46, 0x75, 0x6c, 0x6c, + 0x55, 0x72, 0x69, 0x22, 0xab, 0x01, 0x0a, 0x0a, 0x52, 0x50, 0x43, 0x4d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x12, 0x26, 0x0a, 0x0f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x66, 0x75, 0x6c, + 0x6c, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6d, 0x65, 0x74, + 0x68, 0x6f, 0x64, 0x46, 0x75, 0x6c, 0x6c, 0x55, 0x72, 0x69, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, + 0x72, 0x65, 0x61, 0x6d, 0x5f, 0x72, 0x70, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, + 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x70, 0x63, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, + 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x79, + 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, + 0x69, 0x7a, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x73, 0x65, 0x72, 0x69, + 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x69, 0x73, 0x5f, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x69, 0x73, 0x45, 0x72, 0x72, 0x6f, + 0x72, 0x22, 0xc0, 0x01, 0x0a, 0x15, 0x52, 0x50, 0x43, 0x4d, 0x69, 0x64, 0x64, 0x6c, 0x65, 0x77, + 0x61, 0x72, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x0a, 0x72, + 0x65, 0x66, 0x5f, 0x6d, 0x73, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, + 0x08, 0x72, 0x65, 0x66, 0x4d, 0x73, 0x67, 0x49, 0x64, 0x12, 0x3b, 0x0a, 0x08, 0x72, 0x65, 0x67, + 0x69, 0x73, 0x74, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6c, 0x6e, + 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x69, 0x64, 0x64, 0x6c, 0x65, 0x77, 0x61, 0x72, 0x65, 0x52, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x08, 0x72, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x12, 0x36, 0x0a, 0x08, 0x66, 0x65, 0x65, 0x64, 0x62, 0x61, + 0x63, 0x6b, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, + 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x46, 0x65, 0x65, 0x64, 0x62, 0x61, + 0x63, 0x6b, 0x48, 0x00, 0x52, 0x08, 0x66, 0x65, 0x65, 0x64, 0x62, 0x61, 0x63, 0x6b, 0x42, 0x14, + 0x0a, 0x12, 0x6d, 0x69, 0x64, 0x64, 0x6c, 0x65, 0x77, 0x61, 0x72, 0x65, 0x5f, 0x6d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x22, 0xa6, 0x01, 0x0a, 0x16, 0x4d, 0x69, 0x64, 0x64, 0x6c, 0x65, 0x77, + 0x61, 0x72, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, + 0x27, 0x0a, 0x0f, 0x6d, 0x69, 0x64, 0x64, 0x6c, 0x65, 0x77, 0x61, 0x72, 0x65, 0x5f, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6d, 0x69, 0x64, 0x64, 0x6c, 0x65, + 0x77, 0x61, 0x72, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x3d, 0x0a, 0x1b, 0x63, 0x75, 0x73, 0x74, + 0x6f, 0x6d, 0x5f, 0x6d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x5f, 0x63, 0x61, 0x76, 0x65, + 0x61, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x18, 0x63, + 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x43, 0x61, 0x76, + 0x65, 0x61, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x0e, 0x72, 0x65, 0x61, 0x64, 0x5f, + 0x6f, 0x6e, 0x6c, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x0c, 0x72, 0x65, 0x61, 0x64, 0x4f, 0x6e, 0x6c, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x22, 0x8b, 0x01, + 0x0a, 0x11, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x46, 0x65, 0x65, 0x64, 0x62, + 0x61, 0x63, 0x6b, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x70, + 0x6c, 0x61, 0x63, 0x65, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x0f, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x16, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x15, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x2a, 0xcb, 0x02, 0x0a, 0x10, + 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x54, 0x79, 0x70, 0x65, + 0x12, 0x1b, 0x0a, 0x17, 0x53, 0x43, 0x52, 0x49, 0x50, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, + 0x50, 0x55, 0x42, 0x4b, 0x45, 0x59, 0x5f, 0x48, 0x41, 0x53, 0x48, 0x10, 0x00, 0x12, 0x1b, 0x0a, + 0x17, 0x53, 0x43, 0x52, 0x49, 0x50, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x43, 0x52, + 0x49, 0x50, 0x54, 0x5f, 0x48, 0x41, 0x53, 0x48, 0x10, 0x01, 0x12, 0x26, 0x0a, 0x22, 0x53, 0x43, + 0x52, 0x49, 0x50, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x57, 0x49, 0x54, 0x4e, 0x45, 0x53, + 0x53, 0x5f, 0x56, 0x30, 0x5f, 0x50, 0x55, 0x42, 0x4b, 0x45, 0x59, 0x5f, 0x48, 0x41, 0x53, 0x48, + 0x10, 0x02, 0x12, 0x26, 0x0a, 0x22, 0x53, 0x43, 0x52, 0x49, 0x50, 0x54, 0x5f, 0x54, 0x59, 0x50, + 0x45, 0x5f, 0x57, 0x49, 0x54, 0x4e, 0x45, 0x53, 0x53, 0x5f, 0x56, 0x30, 0x5f, 0x53, 0x43, 0x52, + 0x49, 0x50, 0x54, 0x5f, 0x48, 0x41, 0x53, 0x48, 0x10, 0x03, 0x12, 0x16, 0x0a, 0x12, 0x53, 0x43, + 0x52, 0x49, 0x50, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x50, 0x55, 0x42, 0x4b, 0x45, 0x59, + 0x10, 0x04, 0x12, 0x18, 0x0a, 0x14, 0x53, 0x43, 0x52, 0x49, 0x50, 0x54, 0x5f, 0x54, 0x59, 0x50, + 0x45, 0x5f, 0x4d, 0x55, 0x4c, 0x54, 0x49, 0x53, 0x49, 0x47, 0x10, 0x05, 0x12, 0x18, 0x0a, 0x14, + 0x53, 0x43, 0x52, 0x49, 0x50, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4e, 0x55, 0x4c, 0x4c, + 0x44, 0x41, 0x54, 0x41, 0x10, 0x06, 0x12, 0x1c, 0x0a, 0x18, 0x53, 0x43, 0x52, 0x49, 0x50, 0x54, + 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4e, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x4e, 0x44, 0x41, + 0x52, 0x44, 0x10, 0x07, 0x12, 0x1f, 0x0a, 0x1b, 0x53, 0x43, 0x52, 0x49, 0x50, 0x54, 0x5f, 0x54, + 0x59, 0x50, 0x45, 0x5f, 0x57, 0x49, 0x54, 0x4e, 0x45, 0x53, 0x53, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, + 0x4f, 0x57, 0x4e, 0x10, 0x08, 0x12, 0x22, 0x0a, 0x1e, 0x53, 0x43, 0x52, 0x49, 0x50, 0x54, 0x5f, + 0x54, 0x59, 0x50, 0x45, 0x5f, 0x57, 0x49, 0x54, 0x4e, 0x45, 0x53, 0x53, 0x5f, 0x56, 0x31, 0x5f, + 0x54, 0x41, 0x50, 0x52, 0x4f, 0x4f, 0x54, 0x10, 0x09, 0x2a, 0x62, 0x0a, 0x15, 0x43, 0x6f, 0x69, + 0x6e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, + 0x67, 0x79, 0x12, 0x1e, 0x0a, 0x1a, 0x53, 0x54, 0x52, 0x41, 0x54, 0x45, 0x47, 0x59, 0x5f, 0x55, + 0x53, 0x45, 0x5f, 0x47, 0x4c, 0x4f, 0x42, 0x41, 0x4c, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, + 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x53, 0x54, 0x52, 0x41, 0x54, 0x45, 0x47, 0x59, 0x5f, 0x4c, + 0x41, 0x52, 0x47, 0x45, 0x53, 0x54, 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x54, 0x52, 0x41, + 0x54, 0x45, 0x47, 0x59, 0x5f, 0x52, 0x41, 0x4e, 0x44, 0x4f, 0x4d, 0x10, 0x02, 0x2a, 0xac, 0x01, + 0x0a, 0x0b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x54, 0x79, 0x70, 0x65, 0x12, 0x17, 0x0a, + 0x13, 0x57, 0x49, 0x54, 0x4e, 0x45, 0x53, 0x53, 0x5f, 0x50, 0x55, 0x42, 0x4b, 0x45, 0x59, 0x5f, + 0x48, 0x41, 0x53, 0x48, 0x10, 0x00, 0x12, 0x16, 0x0a, 0x12, 0x4e, 0x45, 0x53, 0x54, 0x45, 0x44, + 0x5f, 0x50, 0x55, 0x42, 0x4b, 0x45, 0x59, 0x5f, 0x48, 0x41, 0x53, 0x48, 0x10, 0x01, 0x12, 0x1e, + 0x0a, 0x1a, 0x55, 0x4e, 0x55, 0x53, 0x45, 0x44, 0x5f, 0x57, 0x49, 0x54, 0x4e, 0x45, 0x53, 0x53, + 0x5f, 0x50, 0x55, 0x42, 0x4b, 0x45, 0x59, 0x5f, 0x48, 0x41, 0x53, 0x48, 0x10, 0x02, 0x12, 0x1d, + 0x0a, 0x19, 0x55, 0x4e, 0x55, 0x53, 0x45, 0x44, 0x5f, 0x4e, 0x45, 0x53, 0x54, 0x45, 0x44, 0x5f, + 0x50, 0x55, 0x42, 0x4b, 0x45, 0x59, 0x5f, 0x48, 0x41, 0x53, 0x48, 0x10, 0x03, 0x12, 0x12, 0x0a, + 0x0e, 0x54, 0x41, 0x50, 0x52, 0x4f, 0x4f, 0x54, 0x5f, 0x50, 0x55, 0x42, 0x4b, 0x45, 0x59, 0x10, + 0x04, 0x12, 0x19, 0x0a, 0x15, 0x55, 0x4e, 0x55, 0x53, 0x45, 0x44, 0x5f, 0x54, 0x41, 0x50, 0x52, + 0x4f, 0x4f, 0x54, 0x5f, 0x50, 0x55, 0x42, 0x4b, 0x45, 0x59, 0x10, 0x05, 0x2a, 0xa8, 0x01, 0x0a, + 0x0e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x6d, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, + 0x1b, 0x0a, 0x17, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x5f, 0x43, 0x4f, 0x4d, 0x4d, 0x49, + 0x54, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, + 0x4c, 0x45, 0x47, 0x41, 0x43, 0x59, 0x10, 0x01, 0x12, 0x15, 0x0a, 0x11, 0x53, 0x54, 0x41, 0x54, + 0x49, 0x43, 0x5f, 0x52, 0x45, 0x4d, 0x4f, 0x54, 0x45, 0x5f, 0x4b, 0x45, 0x59, 0x10, 0x02, 0x12, + 0x0b, 0x0a, 0x07, 0x41, 0x4e, 0x43, 0x48, 0x4f, 0x52, 0x53, 0x10, 0x03, 0x12, 0x19, 0x0a, 0x15, + 0x53, 0x43, 0x52, 0x49, 0x50, 0x54, 0x5f, 0x45, 0x4e, 0x46, 0x4f, 0x52, 0x43, 0x45, 0x44, 0x5f, + 0x4c, 0x45, 0x41, 0x53, 0x45, 0x10, 0x04, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x49, 0x4d, 0x50, 0x4c, + 0x45, 0x5f, 0x54, 0x41, 0x50, 0x52, 0x4f, 0x4f, 0x54, 0x10, 0x05, 0x12, 0x1a, 0x0a, 0x16, 0x53, + 0x49, 0x4d, 0x50, 0x4c, 0x45, 0x5f, 0x54, 0x41, 0x50, 0x52, 0x4f, 0x4f, 0x54, 0x5f, 0x4f, 0x56, + 0x45, 0x52, 0x4c, 0x41, 0x59, 0x10, 0x06, 0x2a, 0x61, 0x0a, 0x09, 0x49, 0x6e, 0x69, 0x74, 0x69, + 0x61, 0x74, 0x6f, 0x72, 0x12, 0x15, 0x0a, 0x11, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41, 0x54, 0x4f, + 0x52, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x13, 0x0a, 0x0f, 0x49, + 0x4e, 0x49, 0x54, 0x49, 0x41, 0x54, 0x4f, 0x52, 0x5f, 0x4c, 0x4f, 0x43, 0x41, 0x4c, 0x10, 0x01, + 0x12, 0x14, 0x0a, 0x10, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41, 0x54, 0x4f, 0x52, 0x5f, 0x52, 0x45, + 0x4d, 0x4f, 0x54, 0x45, 0x10, 0x02, 0x12, 0x12, 0x0a, 0x0e, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41, + 0x54, 0x4f, 0x52, 0x5f, 0x42, 0x4f, 0x54, 0x48, 0x10, 0x03, 0x2a, 0x60, 0x0a, 0x0e, 0x52, 0x65, + 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x10, 0x0a, 0x0c, + 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0a, + 0x0a, 0x06, 0x41, 0x4e, 0x43, 0x48, 0x4f, 0x52, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, 0x49, 0x4e, + 0x43, 0x4f, 0x4d, 0x49, 0x4e, 0x47, 0x5f, 0x48, 0x54, 0x4c, 0x43, 0x10, 0x02, 0x12, 0x11, 0x0a, + 0x0d, 0x4f, 0x55, 0x54, 0x47, 0x4f, 0x49, 0x4e, 0x47, 0x5f, 0x48, 0x54, 0x4c, 0x43, 0x10, 0x03, + 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x4f, 0x4d, 0x4d, 0x49, 0x54, 0x10, 0x04, 0x2a, 0x71, 0x0a, 0x11, + 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x4f, 0x75, 0x74, 0x63, 0x6f, 0x6d, + 0x65, 0x12, 0x13, 0x0a, 0x0f, 0x4f, 0x55, 0x54, 0x43, 0x4f, 0x4d, 0x45, 0x5f, 0x55, 0x4e, 0x4b, + 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x43, 0x4c, 0x41, 0x49, 0x4d, 0x45, + 0x44, 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, 0x55, 0x4e, 0x43, 0x4c, 0x41, 0x49, 0x4d, 0x45, 0x44, + 0x10, 0x02, 0x12, 0x0d, 0x0a, 0x09, 0x41, 0x42, 0x41, 0x4e, 0x44, 0x4f, 0x4e, 0x45, 0x44, 0x10, + 0x03, 0x12, 0x0f, 0x0a, 0x0b, 0x46, 0x49, 0x52, 0x53, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x47, 0x45, + 0x10, 0x04, 0x12, 0x0b, 0x0a, 0x07, 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x10, 0x05, 0x2a, + 0x39, 0x0a, 0x0e, 0x4e, 0x6f, 0x64, 0x65, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x54, 0x79, 0x70, + 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x1a, + 0x0a, 0x16, 0x42, 0x45, 0x54, 0x57, 0x45, 0x45, 0x4e, 0x4e, 0x45, 0x53, 0x53, 0x5f, 0x43, 0x45, + 0x4e, 0x54, 0x52, 0x41, 0x4c, 0x49, 0x54, 0x59, 0x10, 0x01, 0x2a, 0x3b, 0x0a, 0x10, 0x49, 0x6e, + 0x76, 0x6f, 0x69, 0x63, 0x65, 0x48, 0x54, 0x4c, 0x43, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0c, + 0x0a, 0x08, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, + 0x53, 0x45, 0x54, 0x54, 0x4c, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x41, 0x4e, + 0x43, 0x45, 0x4c, 0x45, 0x44, 0x10, 0x02, 0x2a, 0xf6, 0x01, 0x0a, 0x14, 0x50, 0x61, 0x79, 0x6d, + 0x65, 0x6e, 0x74, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, + 0x12, 0x17, 0x0a, 0x13, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, + 0x4f, 0x4e, 0x5f, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x1a, 0x0a, 0x16, 0x46, 0x41, 0x49, + 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x54, 0x49, 0x4d, 0x45, + 0x4f, 0x55, 0x54, 0x10, 0x01, 0x12, 0x1b, 0x0a, 0x17, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, + 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x4e, 0x4f, 0x5f, 0x52, 0x4f, 0x55, 0x54, 0x45, + 0x10, 0x02, 0x12, 0x18, 0x0a, 0x14, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, + 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x12, 0x2c, 0x0a, 0x28, + 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x49, + 0x4e, 0x43, 0x4f, 0x52, 0x52, 0x45, 0x43, 0x54, 0x5f, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, + 0x5f, 0x44, 0x45, 0x54, 0x41, 0x49, 0x4c, 0x53, 0x10, 0x04, 0x12, 0x27, 0x0a, 0x23, 0x46, 0x41, + 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x49, 0x4e, 0x53, + 0x55, 0x46, 0x46, 0x49, 0x43, 0x49, 0x45, 0x4e, 0x54, 0x5f, 0x42, 0x41, 0x4c, 0x41, 0x4e, 0x43, + 0x45, 0x10, 0x05, 0x12, 0x1b, 0x0a, 0x17, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x52, + 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x45, 0x44, 0x10, 0x06, + 0x2a, 0x89, 0x05, 0x0a, 0x0a, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x42, 0x69, 0x74, 0x12, + 0x18, 0x0a, 0x14, 0x44, 0x41, 0x54, 0x41, 0x4c, 0x4f, 0x53, 0x53, 0x5f, 0x50, 0x52, 0x4f, 0x54, + 0x45, 0x43, 0x54, 0x5f, 0x52, 0x45, 0x51, 0x10, 0x00, 0x12, 0x18, 0x0a, 0x14, 0x44, 0x41, 0x54, + 0x41, 0x4c, 0x4f, 0x53, 0x53, 0x5f, 0x50, 0x52, 0x4f, 0x54, 0x45, 0x43, 0x54, 0x5f, 0x4f, 0x50, + 0x54, 0x10, 0x01, 0x12, 0x17, 0x0a, 0x13, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41, 0x4c, 0x5f, 0x52, + 0x4f, 0x55, 0x49, 0x4e, 0x47, 0x5f, 0x53, 0x59, 0x4e, 0x43, 0x10, 0x03, 0x12, 0x1f, 0x0a, 0x1b, + 0x55, 0x50, 0x46, 0x52, 0x4f, 0x4e, 0x54, 0x5f, 0x53, 0x48, 0x55, 0x54, 0x44, 0x4f, 0x57, 0x4e, + 0x5f, 0x53, 0x43, 0x52, 0x49, 0x50, 0x54, 0x5f, 0x52, 0x45, 0x51, 0x10, 0x04, 0x12, 0x1f, 0x0a, + 0x1b, 0x55, 0x50, 0x46, 0x52, 0x4f, 0x4e, 0x54, 0x5f, 0x53, 0x48, 0x55, 0x54, 0x44, 0x4f, 0x57, + 0x4e, 0x5f, 0x53, 0x43, 0x52, 0x49, 0x50, 0x54, 0x5f, 0x4f, 0x50, 0x54, 0x10, 0x05, 0x12, 0x16, + 0x0a, 0x12, 0x47, 0x4f, 0x53, 0x53, 0x49, 0x50, 0x5f, 0x51, 0x55, 0x45, 0x52, 0x49, 0x45, 0x53, + 0x5f, 0x52, 0x45, 0x51, 0x10, 0x06, 0x12, 0x16, 0x0a, 0x12, 0x47, 0x4f, 0x53, 0x53, 0x49, 0x50, + 0x5f, 0x51, 0x55, 0x45, 0x52, 0x49, 0x45, 0x53, 0x5f, 0x4f, 0x50, 0x54, 0x10, 0x07, 0x12, 0x11, + 0x0a, 0x0d, 0x54, 0x4c, 0x56, 0x5f, 0x4f, 0x4e, 0x49, 0x4f, 0x4e, 0x5f, 0x52, 0x45, 0x51, 0x10, + 0x08, 0x12, 0x11, 0x0a, 0x0d, 0x54, 0x4c, 0x56, 0x5f, 0x4f, 0x4e, 0x49, 0x4f, 0x4e, 0x5f, 0x4f, + 0x50, 0x54, 0x10, 0x09, 0x12, 0x1a, 0x0a, 0x16, 0x45, 0x58, 0x54, 0x5f, 0x47, 0x4f, 0x53, 0x53, + 0x49, 0x50, 0x5f, 0x51, 0x55, 0x45, 0x52, 0x49, 0x45, 0x53, 0x5f, 0x52, 0x45, 0x51, 0x10, 0x0a, + 0x12, 0x1a, 0x0a, 0x16, 0x45, 0x58, 0x54, 0x5f, 0x47, 0x4f, 0x53, 0x53, 0x49, 0x50, 0x5f, 0x51, + 0x55, 0x45, 0x52, 0x49, 0x45, 0x53, 0x5f, 0x4f, 0x50, 0x54, 0x10, 0x0b, 0x12, 0x19, 0x0a, 0x15, + 0x53, 0x54, 0x41, 0x54, 0x49, 0x43, 0x5f, 0x52, 0x45, 0x4d, 0x4f, 0x54, 0x45, 0x5f, 0x4b, 0x45, + 0x59, 0x5f, 0x52, 0x45, 0x51, 0x10, 0x0c, 0x12, 0x19, 0x0a, 0x15, 0x53, 0x54, 0x41, 0x54, 0x49, + 0x43, 0x5f, 0x52, 0x45, 0x4d, 0x4f, 0x54, 0x45, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x4f, 0x50, 0x54, + 0x10, 0x0d, 0x12, 0x14, 0x0a, 0x10, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x41, 0x44, + 0x44, 0x52, 0x5f, 0x52, 0x45, 0x51, 0x10, 0x0e, 0x12, 0x14, 0x0a, 0x10, 0x50, 0x41, 0x59, 0x4d, + 0x45, 0x4e, 0x54, 0x5f, 0x41, 0x44, 0x44, 0x52, 0x5f, 0x4f, 0x50, 0x54, 0x10, 0x0f, 0x12, 0x0b, + 0x0a, 0x07, 0x4d, 0x50, 0x50, 0x5f, 0x52, 0x45, 0x51, 0x10, 0x10, 0x12, 0x0b, 0x0a, 0x07, 0x4d, + 0x50, 0x50, 0x5f, 0x4f, 0x50, 0x54, 0x10, 0x11, 0x12, 0x16, 0x0a, 0x12, 0x57, 0x55, 0x4d, 0x42, + 0x4f, 0x5f, 0x43, 0x48, 0x41, 0x4e, 0x4e, 0x45, 0x4c, 0x53, 0x5f, 0x52, 0x45, 0x51, 0x10, 0x12, + 0x12, 0x16, 0x0a, 0x12, 0x57, 0x55, 0x4d, 0x42, 0x4f, 0x5f, 0x43, 0x48, 0x41, 0x4e, 0x4e, 0x45, + 0x4c, 0x53, 0x5f, 0x4f, 0x50, 0x54, 0x10, 0x13, 0x12, 0x0f, 0x0a, 0x0b, 0x41, 0x4e, 0x43, 0x48, + 0x4f, 0x52, 0x53, 0x5f, 0x52, 0x45, 0x51, 0x10, 0x14, 0x12, 0x0f, 0x0a, 0x0b, 0x41, 0x4e, 0x43, + 0x48, 0x4f, 0x52, 0x53, 0x5f, 0x4f, 0x50, 0x54, 0x10, 0x15, 0x12, 0x1d, 0x0a, 0x19, 0x41, 0x4e, + 0x43, 0x48, 0x4f, 0x52, 0x53, 0x5f, 0x5a, 0x45, 0x52, 0x4f, 0x5f, 0x46, 0x45, 0x45, 0x5f, 0x48, + 0x54, 0x4c, 0x43, 0x5f, 0x52, 0x45, 0x51, 0x10, 0x16, 0x12, 0x1d, 0x0a, 0x19, 0x41, 0x4e, 0x43, + 0x48, 0x4f, 0x52, 0x53, 0x5f, 0x5a, 0x45, 0x52, 0x4f, 0x5f, 0x46, 0x45, 0x45, 0x5f, 0x48, 0x54, + 0x4c, 0x43, 0x5f, 0x4f, 0x50, 0x54, 0x10, 0x17, 0x12, 0x1b, 0x0a, 0x17, 0x52, 0x4f, 0x55, 0x54, + 0x45, 0x5f, 0x42, 0x4c, 0x49, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x5f, 0x52, 0x45, 0x51, 0x55, 0x49, + 0x52, 0x45, 0x44, 0x10, 0x18, 0x12, 0x1b, 0x0a, 0x17, 0x52, 0x4f, 0x55, 0x54, 0x45, 0x5f, 0x42, + 0x4c, 0x49, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x5f, 0x4f, 0x50, 0x54, 0x49, 0x4f, 0x4e, 0x41, 0x4c, + 0x10, 0x19, 0x12, 0x0b, 0x0a, 0x07, 0x41, 0x4d, 0x50, 0x5f, 0x52, 0x45, 0x51, 0x10, 0x1e, 0x12, + 0x0b, 0x0a, 0x07, 0x41, 0x4d, 0x50, 0x5f, 0x4f, 0x50, 0x54, 0x10, 0x1f, 0x2a, 0xac, 0x01, 0x0a, + 0x0d, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x12, 0x1a, + 0x0a, 0x16, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, + 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x1a, 0x0a, 0x16, 0x55, 0x50, + 0x44, 0x41, 0x54, 0x45, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x50, 0x45, 0x4e, + 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x1c, 0x0a, 0x18, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, + 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x46, 0x4f, 0x55, + 0x4e, 0x44, 0x10, 0x02, 0x12, 0x1f, 0x0a, 0x1b, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x5f, 0x46, + 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x5f, + 0x45, 0x52, 0x52, 0x10, 0x03, 0x12, 0x24, 0x0a, 0x20, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x5f, + 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x5f, + 0x50, 0x41, 0x52, 0x41, 0x4d, 0x45, 0x54, 0x45, 0x52, 0x10, 0x04, 0x32, 0x96, 0x2b, 0x0a, 0x09, + 0x4c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x12, 0x4a, 0x0a, 0x0d, 0x57, 0x61, 0x6c, + 0x6c, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x1b, 0x2e, 0x6c, 0x6e, 0x72, + 0x70, 0x63, 0x2e, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, + 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4d, 0x0a, 0x0e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, + 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x1c, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, + 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, + 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, + 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1d, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, + 0x47, 0x65, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x54, + 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, + 0x73, 0x12, 0x44, 0x0a, 0x0b, 0x45, 0x73, 0x74, 0x69, 0x6d, 0x61, 0x74, 0x65, 0x46, 0x65, 0x65, + 0x12, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x45, 0x73, 0x74, 0x69, 0x6d, 0x61, 0x74, + 0x65, 0x46, 0x65, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6c, 0x6e, + 0x72, 0x70, 0x63, 0x2e, 0x45, 0x73, 0x74, 0x69, 0x6d, 0x61, 0x74, 0x65, 0x46, 0x65, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x09, 0x53, 0x65, 0x6e, 0x64, 0x43, + 0x6f, 0x69, 0x6e, 0x73, 0x12, 0x17, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, + 0x64, 0x43, 0x6f, 0x69, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, + 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f, 0x69, 0x6e, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x55, + 0x6e, 0x73, 0x70, 0x65, 0x6e, 0x74, 0x12, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4c, + 0x69, 0x73, 0x74, 0x55, 0x6e, 0x73, 0x70, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x1a, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x6e, + 0x73, 0x70, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, + 0x15, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1d, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x47, + 0x65, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x72, + 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x30, 0x01, 0x12, 0x3b, 0x0a, 0x08, 0x53, + 0x65, 0x6e, 0x64, 0x4d, 0x61, 0x6e, 0x79, 0x12, 0x16, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, + 0x53, 0x65, 0x6e, 0x64, 0x4d, 0x61, 0x6e, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x17, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x4d, 0x61, 0x6e, 0x79, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x0a, 0x4e, 0x65, 0x77, 0x41, + 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x18, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4e, + 0x65, 0x77, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4e, 0x65, 0x77, 0x41, 0x64, 0x64, 0x72, + 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x0b, 0x53, + 0x69, 0x67, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x19, 0x2e, 0x6c, 0x6e, 0x72, + 0x70, 0x63, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x69, + 0x67, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x4a, 0x0a, 0x0d, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x4d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x12, 0x1b, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, + 0x79, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x1c, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x4d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, + 0x0b, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x50, 0x65, 0x65, 0x72, 0x12, 0x19, 0x2e, 0x6c, + 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x50, 0x65, 0x65, 0x72, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, + 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x4d, 0x0a, 0x0e, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, + 0x74, 0x50, 0x65, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x44, 0x69, + 0x73, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x44, 0x69, 0x73, 0x63, + 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, + 0x17, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x65, 0x65, 0x72, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, + 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x65, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x47, 0x0a, 0x13, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x50, + 0x65, 0x65, 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x1c, 0x2e, 0x6c, 0x6e, 0x72, 0x70, + 0x63, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x75, 0x62, 0x73, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x10, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, + 0x50, 0x65, 0x65, 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x12, 0x38, 0x0a, 0x07, 0x47, + 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x15, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x47, + 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, + 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x47, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x44, 0x65, 0x62, 0x75, + 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1a, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x47, 0x65, + 0x74, 0x44, 0x65, 0x62, 0x75, 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x1b, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x62, + 0x75, 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x50, + 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x49, 0x6e, 0x66, + 0x6f, 0x12, 0x1d, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x63, + 0x6f, 0x76, 0x65, 0x72, 0x79, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1e, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x63, 0x6f, + 0x76, 0x65, 0x72, 0x79, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x50, 0x0a, 0x0f, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x43, 0x68, 0x61, 0x6e, 0x6e, + 0x65, 0x6c, 0x73, 0x12, 0x1d, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x65, 0x6e, 0x64, + 0x69, 0x6e, 0x67, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x65, 0x6e, 0x64, 0x69, + 0x6e, 0x67, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x47, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, + 0x6c, 0x73, 0x12, 0x1a, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, + 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, + 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x6e, + 0x65, 0x6c, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x56, 0x0a, 0x16, 0x53, + 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x45, + 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x1f, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, + 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, + 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, + 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x30, 0x01, 0x12, 0x4d, 0x0a, 0x0e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x64, 0x43, 0x68, 0x61, + 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x12, 0x1c, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6c, + 0x6f, 0x73, 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6c, 0x6f, 0x73, + 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x41, 0x0a, 0x0f, 0x4f, 0x70, 0x65, 0x6e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, + 0x6c, 0x53, 0x79, 0x6e, 0x63, 0x12, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4f, 0x70, + 0x65, 0x6e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x13, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, + 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x43, 0x0a, 0x0b, 0x4f, 0x70, 0x65, 0x6e, 0x43, 0x68, 0x61, + 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4f, 0x70, 0x65, + 0x6e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x17, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4f, 0x70, 0x65, 0x6e, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x30, 0x01, 0x12, 0x53, 0x0a, 0x10, 0x42, 0x61, + 0x74, 0x63, 0x68, 0x4f, 0x70, 0x65, 0x6e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x1e, + 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x4f, 0x70, 0x65, 0x6e, + 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, + 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x4f, 0x70, 0x65, 0x6e, + 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x4c, 0x0a, 0x10, 0x46, 0x75, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x65, 0x53, + 0x74, 0x65, 0x70, 0x12, 0x1b, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x75, 0x6e, 0x64, + 0x69, 0x6e, 0x67, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x73, 0x67, + 0x1a, 0x1b, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x75, 0x6e, 0x64, 0x69, 0x6e, 0x67, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x53, 0x74, 0x65, 0x70, 0x52, 0x65, 0x73, 0x70, 0x12, 0x50, 0x0a, + 0x0f, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x6f, 0x72, + 0x12, 0x1c, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, + 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x1a, 0x1b, + 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x41, 0x63, + 0x63, 0x65, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x28, 0x01, 0x30, 0x01, 0x12, + 0x46, 0x0a, 0x0c, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x12, + 0x1a, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x43, 0x68, 0x61, + 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x6c, 0x6e, + 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x30, 0x01, 0x12, 0x4d, 0x0a, 0x0e, 0x41, 0x62, 0x61, 0x6e, 0x64, + 0x6f, 0x6e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x1c, 0x2e, 0x6c, 0x6e, 0x72, 0x70, + 0x63, 0x2e, 0x41, 0x62, 0x61, 0x6e, 0x64, 0x6f, 0x6e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, + 0x41, 0x62, 0x61, 0x6e, 0x64, 0x6f, 0x6e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3f, 0x0a, 0x0b, 0x53, 0x65, 0x6e, 0x64, 0x50, 0x61, + 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x12, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, + 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x6c, 0x6e, 0x72, 0x70, + 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, + 0x88, 0x02, 0x01, 0x28, 0x01, 0x30, 0x01, 0x12, 0x3f, 0x0a, 0x0f, 0x53, 0x65, 0x6e, 0x64, 0x50, + 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x79, 0x6e, 0x63, 0x12, 0x12, 0x2e, 0x6c, 0x6e, 0x72, + 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, + 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x03, 0x88, 0x02, 0x01, 0x12, 0x46, 0x0a, 0x0b, 0x53, 0x65, 0x6e, 0x64, + 0x54, 0x6f, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, + 0x53, 0x65, 0x6e, 0x64, 0x54, 0x6f, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x88, 0x02, 0x01, 0x28, 0x01, 0x30, 0x01, + 0x12, 0x46, 0x0a, 0x0f, 0x53, 0x65, 0x6e, 0x64, 0x54, 0x6f, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x53, + 0x79, 0x6e, 0x63, 0x12, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, + 0x54, 0x6f, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, + 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x03, 0x88, 0x02, 0x01, 0x12, 0x37, 0x0a, 0x0a, 0x41, 0x64, 0x64, 0x49, + 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x0e, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x49, + 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x1a, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x41, + 0x64, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x45, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, + 0x73, 0x12, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x49, 0x6e, + 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6c, + 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x0d, 0x4c, 0x6f, 0x6f, 0x6b, + 0x75, 0x70, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x12, 0x2e, 0x6c, 0x6e, 0x72, 0x70, + 0x63, 0x2e, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x48, 0x61, 0x73, 0x68, 0x1a, 0x0e, 0x2e, + 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x41, 0x0a, + 0x11, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, + 0x65, 0x73, 0x12, 0x1a, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x76, 0x6f, 0x69, + 0x63, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x0e, + 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x30, 0x01, + 0x12, 0x54, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, + 0x65, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x1c, 0x2e, 0x6c, 0x6e, 0x72, 0x70, + 0x63, 0x2e, 0x44, 0x65, 0x6c, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x65, 0x64, 0x49, 0x6e, 0x76, + 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x1a, 0x1d, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, + 0x44, 0x65, 0x6c, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x65, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, + 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x12, 0x32, 0x0a, 0x0c, 0x44, 0x65, 0x63, 0x6f, 0x64, 0x65, + 0x50, 0x61, 0x79, 0x52, 0x65, 0x71, 0x12, 0x13, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, + 0x61, 0x79, 0x52, 0x65, 0x71, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x1a, 0x0d, 0x2e, 0x6c, 0x6e, + 0x72, 0x70, 0x63, 0x2e, 0x50, 0x61, 0x79, 0x52, 0x65, 0x71, 0x12, 0x47, 0x0a, 0x0c, 0x4c, 0x69, + 0x73, 0x74, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x1a, 0x2e, 0x6c, 0x6e, 0x72, + 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4c, + 0x69, 0x73, 0x74, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x62, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x61, 0x79, 0x6d, 0x65, + 0x6e, 0x74, 0x44, 0x75, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x12, 0x23, 0x2e, 0x6c, + 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, + 0x44, 0x75, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x24, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x61, + 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x44, 0x75, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6b, 0x0a, 0x18, 0x4c, 0x69, 0x73, 0x74, 0x41, + 0x6c, 0x6c, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x44, 0x75, 0x70, 0x6c, 0x69, 0x63, 0x61, + 0x74, 0x65, 0x73, 0x12, 0x26, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x41, 0x6c, 0x6c, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x44, 0x75, 0x70, 0x6c, 0x69, 0x63, + 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x6c, 0x6e, + 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x6c, 0x6c, 0x50, 0x61, 0x79, 0x6d, 0x65, + 0x6e, 0x74, 0x44, 0x75, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4a, 0x0a, 0x0d, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x61, + 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x1b, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x56, 0x0a, 0x11, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x6c, 0x6c, 0x50, 0x61, 0x79, + 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x1f, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x41, 0x6c, 0x6c, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x44, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x6c, 0x6c, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, 0x0d, 0x44, 0x65, 0x73, 0x63, + 0x72, 0x69, 0x62, 0x65, 0x47, 0x72, 0x61, 0x70, 0x68, 0x12, 0x1a, 0x2e, 0x6c, 0x6e, 0x72, 0x70, + 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x47, 0x72, 0x61, 0x70, 0x68, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, + 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x47, 0x72, 0x61, 0x70, 0x68, 0x12, 0x47, 0x0a, 0x0e, 0x47, 0x65, + 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x12, 0x19, 0x2e, 0x6c, + 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, + 0x4e, 0x6f, 0x64, 0x65, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x39, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x49, 0x6e, + 0x66, 0x6f, 0x12, 0x16, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x49, + 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x6c, 0x6e, 0x72, + 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x45, 0x64, 0x67, 0x65, 0x12, 0x36, + 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x16, 0x2e, + 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0f, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4e, 0x6f, + 0x64, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x44, 0x0a, 0x0b, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, + 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, + 0x65, 0x72, 0x79, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1a, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x6f, + 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3f, 0x0a, 0x0e, + 0x47, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x19, + 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x6e, + 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x6c, 0x6e, 0x72, 0x70, + 0x63, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x35, 0x0a, + 0x0a, 0x53, 0x74, 0x6f, 0x70, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x12, 0x12, 0x2e, 0x6c, 0x6e, + 0x72, 0x70, 0x63, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x13, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x57, 0x0a, 0x15, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, + 0x65, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x47, 0x72, 0x61, 0x70, 0x68, 0x12, 0x20, 0x2e, + 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x47, 0x72, 0x61, 0x70, 0x68, 0x54, 0x6f, 0x70, 0x6f, 0x6c, + 0x6f, 0x67, 0x79, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x1a, + 0x1a, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x47, 0x72, 0x61, 0x70, 0x68, 0x54, 0x6f, 0x70, + 0x6f, 0x6c, 0x6f, 0x67, 0x79, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x30, 0x01, 0x12, 0x41, 0x0a, + 0x0a, 0x44, 0x65, 0x62, 0x75, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x18, 0x2e, 0x6c, 0x6e, + 0x72, 0x70, 0x63, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x44, 0x65, + 0x62, 0x75, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x3e, 0x0a, 0x09, 0x46, 0x65, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x17, 0x2e, + 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x65, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x46, + 0x65, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x4e, 0x0a, 0x13, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, + 0x6c, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x1a, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, + 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x56, 0x0a, 0x11, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x48, 0x69, + 0x73, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x1f, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x6f, + 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x46, + 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, 0x13, 0x45, 0x78, 0x70, 0x6f, + 0x72, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x12, + 0x21, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x43, 0x68, + 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, + 0x65, 0x6c, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x12, 0x54, 0x0a, 0x17, 0x45, 0x78, 0x70, 0x6f, + 0x72, 0x74, 0x41, 0x6c, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x42, 0x61, 0x63, 0x6b, + 0x75, 0x70, 0x73, 0x12, 0x1e, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, + 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, + 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x4e, + 0x0a, 0x10, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x43, 0x68, 0x61, 0x6e, 0x42, 0x61, 0x63, 0x6b, + 0x75, 0x70, 0x12, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x42, + 0x61, 0x63, 0x6b, 0x75, 0x70, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x1a, 0x1f, 0x2e, + 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x43, 0x68, 0x61, 0x6e, + 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x56, + 0x0a, 0x15, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, + 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x12, 0x1f, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, + 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x75, + 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, + 0x2e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x58, 0x0a, 0x17, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, + 0x69, 0x62, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, + 0x73, 0x12, 0x20, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, + 0x6c, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, - 0x6e, 0x65, 0x6c, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x30, 0x01, - 0x12, 0x4d, 0x0a, 0x0e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, - 0x6c, 0x73, 0x12, 0x1c, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, - 0x64, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x1d, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x64, 0x43, - 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x41, 0x0a, 0x0f, 0x4f, 0x70, 0x65, 0x6e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x53, 0x79, - 0x6e, 0x63, 0x12, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4f, 0x70, 0x65, 0x6e, 0x43, - 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, - 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x50, 0x6f, 0x69, - 0x6e, 0x74, 0x12, 0x43, 0x0a, 0x0b, 0x4f, 0x70, 0x65, 0x6e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, - 0x6c, 0x12, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4f, 0x70, 0x65, 0x6e, 0x43, 0x68, - 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x6c, - 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4f, 0x70, 0x65, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x30, 0x01, 0x12, 0x53, 0x0a, 0x10, 0x42, 0x61, 0x74, 0x63, 0x68, - 0x4f, 0x70, 0x65, 0x6e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x1e, 0x2e, 0x6c, 0x6e, - 0x72, 0x70, 0x63, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x4f, 0x70, 0x65, 0x6e, 0x43, 0x68, 0x61, - 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x6c, 0x6e, - 0x72, 0x70, 0x63, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x4f, 0x70, 0x65, 0x6e, 0x43, 0x68, 0x61, - 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x10, - 0x46, 0x75, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x65, 0x53, 0x74, 0x65, 0x70, - 0x12, 0x1b, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x75, 0x6e, 0x64, 0x69, 0x6e, 0x67, - 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x73, 0x67, 0x1a, 0x1b, 0x2e, - 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x75, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, - 0x74, 0x65, 0x53, 0x74, 0x65, 0x70, 0x52, 0x65, 0x73, 0x70, 0x12, 0x50, 0x0a, 0x0f, 0x43, 0x68, - 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x6f, 0x72, 0x12, 0x1c, 0x2e, - 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x41, 0x63, 0x63, - 0x65, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x1a, 0x1b, 0x2e, 0x6c, 0x6e, - 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x41, 0x63, 0x63, 0x65, 0x70, - 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x28, 0x01, 0x30, 0x01, 0x12, 0x46, 0x0a, 0x0c, - 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x6c, - 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, - 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, - 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x30, 0x01, 0x12, 0x4d, 0x0a, 0x0e, 0x41, 0x62, 0x61, 0x6e, 0x64, 0x6f, 0x6e, 0x43, - 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x1c, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x41, - 0x62, 0x61, 0x6e, 0x64, 0x6f, 0x6e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x62, 0x61, - 0x6e, 0x64, 0x6f, 0x6e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x3f, 0x0a, 0x0b, 0x53, 0x65, 0x6e, 0x64, 0x50, 0x61, 0x79, 0x6d, 0x65, - 0x6e, 0x74, 0x12, 0x12, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, - 0x65, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x88, 0x02, 0x01, - 0x28, 0x01, 0x30, 0x01, 0x12, 0x3f, 0x0a, 0x0f, 0x53, 0x65, 0x6e, 0x64, 0x50, 0x61, 0x79, 0x6d, - 0x65, 0x6e, 0x74, 0x53, 0x79, 0x6e, 0x63, 0x12, 0x12, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, - 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x6c, 0x6e, - 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x03, 0x88, 0x02, 0x01, 0x12, 0x46, 0x0a, 0x0b, 0x53, 0x65, 0x6e, 0x64, 0x54, 0x6f, 0x52, - 0x6f, 0x75, 0x74, 0x65, 0x12, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, - 0x64, 0x54, 0x6f, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x13, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x88, 0x02, 0x01, 0x28, 0x01, 0x30, 0x01, 0x12, 0x46, 0x0a, - 0x0f, 0x53, 0x65, 0x6e, 0x64, 0x54, 0x6f, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x53, 0x79, 0x6e, 0x63, - 0x12, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x54, 0x6f, 0x52, - 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x6c, 0x6e, - 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x03, 0x88, 0x02, 0x01, 0x12, 0x37, 0x0a, 0x0a, 0x41, 0x64, 0x64, 0x49, 0x6e, 0x76, 0x6f, - 0x69, 0x63, 0x65, 0x12, 0x0e, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x76, 0x6f, - 0x69, 0x63, 0x65, 0x1a, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x49, - 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x45, - 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x12, 0x19, - 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x49, 0x6e, 0x76, 0x6f, 0x69, - 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6c, 0x6e, 0x72, 0x70, - 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x0d, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x49, - 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x12, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, - 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x48, 0x61, 0x73, 0x68, 0x1a, 0x0e, 0x2e, 0x6c, 0x6e, 0x72, - 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x41, 0x0a, 0x11, 0x53, 0x75, - 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x12, - 0x1a, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x53, - 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x0e, 0x2e, 0x6c, 0x6e, - 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x30, 0x01, 0x12, 0x54, 0x0a, - 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x65, 0x64, 0x49, - 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x1c, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x44, - 0x65, 0x6c, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x65, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, - 0x65, 0x52, 0x65, 0x71, 0x1a, 0x1d, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x44, 0x65, 0x6c, - 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x65, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, - 0x65, 0x73, 0x70, 0x12, 0x32, 0x0a, 0x0c, 0x44, 0x65, 0x63, 0x6f, 0x64, 0x65, 0x50, 0x61, 0x79, - 0x52, 0x65, 0x71, 0x12, 0x13, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x61, 0x79, 0x52, - 0x65, 0x71, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x1a, 0x0d, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, - 0x2e, 0x50, 0x61, 0x79, 0x52, 0x65, 0x71, 0x12, 0x47, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x50, - 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x1a, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, - 0x4c, 0x69, 0x73, 0x74, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, - 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x4a, 0x0a, 0x0d, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, - 0x74, 0x12, 0x1b, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, - 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x61, 0x79, - 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x56, 0x0a, 0x11, - 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x6c, 0x6c, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, - 0x73, 0x12, 0x1f, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x41, 0x6c, 0x6c, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x41, 0x6c, 0x6c, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, 0x0d, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, - 0x47, 0x72, 0x61, 0x70, 0x68, 0x12, 0x1a, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, - 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x47, 0x72, 0x61, 0x70, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x13, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, - 0x6c, 0x47, 0x72, 0x61, 0x70, 0x68, 0x12, 0x47, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x4e, 0x6f, 0x64, - 0x65, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x12, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, - 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4e, 0x6f, 0x64, 0x65, - 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x39, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x16, - 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, - 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x45, 0x64, 0x67, 0x65, 0x12, 0x36, 0x0a, 0x0b, 0x47, 0x65, - 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x16, 0x2e, 0x6c, 0x6e, 0x72, 0x70, - 0x63, 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x0f, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x6e, - 0x66, 0x6f, 0x12, 0x44, 0x0a, 0x0b, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x6f, 0x75, 0x74, 0x65, - 0x73, 0x12, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, - 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6c, - 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3f, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x4e, - 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x19, 0x2e, 0x6c, 0x6e, 0x72, - 0x70, 0x63, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4e, 0x65, - 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x35, 0x0a, 0x0a, 0x53, 0x74, 0x6f, - 0x70, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x12, 0x12, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, - 0x53, 0x74, 0x6f, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x6c, 0x6e, - 0x72, 0x70, 0x63, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x57, 0x0a, 0x15, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x43, 0x68, 0x61, - 0x6e, 0x6e, 0x65, 0x6c, 0x47, 0x72, 0x61, 0x70, 0x68, 0x12, 0x20, 0x2e, 0x6c, 0x6e, 0x72, 0x70, - 0x63, 0x2e, 0x47, 0x72, 0x61, 0x70, 0x68, 0x54, 0x6f, 0x70, 0x6f, 0x6c, 0x6f, 0x67, 0x79, 0x53, - 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x1a, 0x2e, 0x6c, 0x6e, - 0x72, 0x70, 0x63, 0x2e, 0x47, 0x72, 0x61, 0x70, 0x68, 0x54, 0x6f, 0x70, 0x6f, 0x6c, 0x6f, 0x67, - 0x79, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x30, 0x01, 0x12, 0x41, 0x0a, 0x0a, 0x44, 0x65, 0x62, - 0x75, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x18, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, - 0x44, 0x65, 0x62, 0x75, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x4c, - 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x09, - 0x46, 0x65, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x17, 0x2e, 0x6c, 0x6e, 0x72, 0x70, - 0x63, 0x2e, 0x46, 0x65, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x65, 0x65, 0x52, 0x65, - 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, 0x13, - 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x50, 0x6f, 0x6c, - 0x69, 0x63, 0x79, 0x12, 0x1a, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x1b, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x56, 0x0a, 0x11, - 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, - 0x79, 0x12, 0x1f, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, - 0x64, 0x69, 0x6e, 0x67, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, - 0x72, 0x64, 0x69, 0x6e, 0x67, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, 0x13, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x43, 0x68, - 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x12, 0x21, 0x2e, 0x6c, 0x6e, - 0x72, 0x70, 0x63, 0x2e, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, - 0x6c, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, - 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x42, 0x61, - 0x63, 0x6b, 0x75, 0x70, 0x12, 0x54, 0x0a, 0x17, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x41, 0x6c, - 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x12, - 0x1e, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x42, 0x61, 0x63, 0x6b, - 0x75, 0x70, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x42, 0x61, 0x63, 0x6b, - 0x75, 0x70, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x4e, 0x0a, 0x10, 0x56, 0x65, - 0x72, 0x69, 0x66, 0x79, 0x43, 0x68, 0x61, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x12, 0x19, - 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x75, - 0x70, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x1a, 0x1f, 0x2e, 0x6c, 0x6e, 0x72, 0x70, - 0x63, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x43, 0x68, 0x61, 0x6e, 0x42, 0x61, 0x63, 0x6b, - 0x75, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x56, 0x0a, 0x15, 0x52, 0x65, - 0x73, 0x74, 0x6f, 0x72, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x42, 0x61, 0x63, 0x6b, - 0x75, 0x70, 0x73, 0x12, 0x1f, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x73, 0x74, - 0x6f, 0x72, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x73, - 0x74, 0x6f, 0x72, 0x65, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x58, 0x0a, 0x17, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x43, - 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x12, 0x20, 0x2e, - 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x42, 0x61, 0x63, - 0x6b, 0x75, 0x70, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x1a, - 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x42, 0x61, 0x63, 0x6b, - 0x75, 0x70, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x30, 0x01, 0x12, 0x47, 0x0a, 0x0c, - 0x42, 0x61, 0x6b, 0x65, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x12, 0x1a, 0x2e, 0x6c, + 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x30, 0x01, + 0x12, 0x47, 0x0a, 0x0c, 0x42, 0x61, 0x6b, 0x65, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, + 0x12, 0x1a, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x42, 0x61, 0x6b, 0x65, 0x4d, 0x61, 0x63, + 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x42, 0x61, 0x6b, 0x65, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, - 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, - 0x2e, 0x42, 0x61, 0x6b, 0x65, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x50, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, - 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x49, 0x44, 0x73, 0x12, 0x1d, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, - 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x49, 0x44, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, - 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x49, 0x44, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x10, 0x44, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x1e, 0x2e, 0x6c, 0x6e, - 0x72, 0x70, 0x63, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, - 0x6f, 0x6e, 0x49, 0x44, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x6c, 0x6e, - 0x72, 0x70, 0x63, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, - 0x6f, 0x6e, 0x49, 0x44, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x50, 0x0a, 0x0f, - 0x4c, 0x69, 0x73, 0x74, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, - 0x1d, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x65, 0x72, 0x6d, - 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, - 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x65, 0x72, 0x6d, 0x69, - 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x53, - 0x0a, 0x18, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x50, - 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1a, 0x2e, 0x6c, 0x6e, 0x72, + 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x50, 0x0a, 0x0f, 0x4c, 0x69, 0x73, + 0x74, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x49, 0x44, 0x73, 0x12, 0x1d, 0x2e, 0x6c, + 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, + 0x6e, 0x49, 0x44, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x6c, 0x6e, + 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, + 0x49, 0x44, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x10, 0x44, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x49, 0x44, 0x12, + 0x1e, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, + 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x49, 0x44, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x1f, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, + 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x49, 0x44, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x50, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x73, 0x12, 0x1d, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, + 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x53, 0x0a, 0x18, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4d, 0x61, 0x63, 0x61, 0x72, + 0x6f, 0x6f, 0x6e, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1a, + 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4d, 0x61, 0x63, 0x50, + 0x65, 0x72, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4d, 0x61, 0x63, 0x50, 0x65, 0x72, 0x6d, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, - 0x68, 0x65, 0x63, 0x6b, 0x4d, 0x61, 0x63, 0x50, 0x65, 0x72, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x56, 0x0a, 0x15, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, - 0x50, 0x43, 0x4d, 0x69, 0x64, 0x64, 0x6c, 0x65, 0x77, 0x61, 0x72, 0x65, 0x12, 0x1c, 0x2e, 0x6c, - 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x50, 0x43, 0x4d, 0x69, 0x64, 0x64, 0x6c, 0x65, 0x77, 0x61, - 0x72, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x1a, 0x1b, 0x2e, 0x6c, 0x6e, 0x72, - 0x70, 0x63, 0x2e, 0x52, 0x50, 0x43, 0x4d, 0x69, 0x64, 0x64, 0x6c, 0x65, 0x77, 0x61, 0x72, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x28, 0x01, 0x30, 0x01, 0x12, 0x56, 0x0a, 0x11, 0x53, - 0x65, 0x6e, 0x64, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, - 0x12, 0x1f, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x75, 0x73, - 0x74, 0x6f, 0x6d, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x20, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x75, - 0x73, 0x74, 0x6f, 0x6d, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x58, 0x0a, 0x17, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, - 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x12, 0x25, - 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, - 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x75, - 0x73, 0x74, 0x6f, 0x6d, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x30, 0x01, 0x12, 0x53, 0x0a, - 0x10, 0x53, 0x65, 0x6e, 0x64, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, - 0x65, 0x12, 0x1e, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x4f, 0x6e, - 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x1f, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x4f, 0x6e, - 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x55, 0x0a, 0x16, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x4f, - 0x6e, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x12, 0x24, 0x2e, 0x6c, - 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x4f, 0x6e, - 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, - 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x30, 0x01, 0x12, 0x44, 0x0a, 0x0b, 0x4c, 0x69, 0x73, - 0x74, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x73, 0x12, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, - 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, - 0x41, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x5f, 0x0a, 0x14, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x48, 0x74, 0x6c, 0x63, 0x52, 0x65, 0x73, - 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x22, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, - 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x48, 0x74, 0x6c, 0x63, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x75, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x6c, 0x6e, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x56, 0x0a, 0x15, 0x52, 0x65, 0x67, 0x69, 0x73, + 0x74, 0x65, 0x72, 0x52, 0x50, 0x43, 0x4d, 0x69, 0x64, 0x64, 0x6c, 0x65, 0x77, 0x61, 0x72, 0x65, + 0x12, 0x1c, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x50, 0x43, 0x4d, 0x69, 0x64, 0x64, + 0x6c, 0x65, 0x77, 0x61, 0x72, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x1a, 0x1b, + 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x50, 0x43, 0x4d, 0x69, 0x64, 0x64, 0x6c, 0x65, + 0x77, 0x61, 0x72, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x28, 0x01, 0x30, 0x01, 0x12, + 0x56, 0x0a, 0x11, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x4d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x12, 0x1f, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, + 0x64, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, + 0x6e, 0x64, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x58, 0x0a, 0x17, 0x53, 0x75, 0x62, 0x73, 0x63, + 0x72, 0x69, 0x62, 0x65, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x73, 0x12, 0x25, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, + 0x72, 0x69, 0x62, 0x65, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x6c, 0x6e, 0x72, 0x70, + 0x63, 0x2e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x30, + 0x01, 0x12, 0x53, 0x0a, 0x10, 0x53, 0x65, 0x6e, 0x64, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x4d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1e, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, + 0x6e, 0x64, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, + 0x6e, 0x64, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x55, 0x0a, 0x16, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, + 0x69, 0x62, 0x65, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, + 0x12, 0x24, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, + 0x62, 0x65, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4f, + 0x6e, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x30, 0x01, 0x12, 0x44, 0x0a, + 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x73, 0x12, 0x19, 0x2e, 0x6c, + 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, + 0x4c, 0x69, 0x73, 0x74, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x5f, 0x0a, 0x14, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x48, 0x74, 0x6c, + 0x63, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x22, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x48, 0x74, 0x6c, 0x63, 0x52, 0x65, - 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x42, 0x27, 0x5a, 0x25, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, - 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2f, - 0x6c, 0x6e, 0x64, 0x2f, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x33, + 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x23, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x48, 0x74, + 0x6c, 0x63, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x27, 0x5a, 0x25, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6e, 0x65, 0x74, 0x77, + 0x6f, 0x72, 0x6b, 0x2f, 0x6c, 0x6e, 0x64, 0x2f, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -22485,7 +22808,7 @@ func file_lightning_proto_rawDescGZIP() []byte { } var file_lightning_proto_enumTypes = make([]protoimpl.EnumInfo, 21) -var file_lightning_proto_msgTypes = make([]protoimpl.MessageInfo, 236) +var file_lightning_proto_msgTypes = make([]protoimpl.MessageInfo, 241) var file_lightning_proto_goTypes = []interface{}{ (OutputScriptType)(0), // 0: lnrpc.OutputScriptType (CoinSelectionStrategy)(0), // 1: lnrpc.CoinSelectionStrategy @@ -22662,88 +22985,93 @@ var file_lightning_proto_goTypes = []interface{}{ (*HTLCAttempt)(nil), // 172: lnrpc.HTLCAttempt (*ListPaymentsRequest)(nil), // 173: lnrpc.ListPaymentsRequest (*ListPaymentsResponse)(nil), // 174: lnrpc.ListPaymentsResponse - (*DeletePaymentRequest)(nil), // 175: lnrpc.DeletePaymentRequest - (*DeleteAllPaymentsRequest)(nil), // 176: lnrpc.DeleteAllPaymentsRequest - (*DeletePaymentResponse)(nil), // 177: lnrpc.DeletePaymentResponse - (*DeleteAllPaymentsResponse)(nil), // 178: lnrpc.DeleteAllPaymentsResponse - (*AbandonChannelRequest)(nil), // 179: lnrpc.AbandonChannelRequest - (*AbandonChannelResponse)(nil), // 180: lnrpc.AbandonChannelResponse - (*DebugLevelRequest)(nil), // 181: lnrpc.DebugLevelRequest - (*DebugLevelResponse)(nil), // 182: lnrpc.DebugLevelResponse - (*PayReqString)(nil), // 183: lnrpc.PayReqString - (*PayReq)(nil), // 184: lnrpc.PayReq - (*Feature)(nil), // 185: lnrpc.Feature - (*FeeReportRequest)(nil), // 186: lnrpc.FeeReportRequest - (*ChannelFeeReport)(nil), // 187: lnrpc.ChannelFeeReport - (*FeeReportResponse)(nil), // 188: lnrpc.FeeReportResponse - (*InboundFee)(nil), // 189: lnrpc.InboundFee - (*PolicyUpdateRequest)(nil), // 190: lnrpc.PolicyUpdateRequest - (*FailedUpdate)(nil), // 191: lnrpc.FailedUpdate - (*PolicyUpdateResponse)(nil), // 192: lnrpc.PolicyUpdateResponse - (*ForwardingHistoryRequest)(nil), // 193: lnrpc.ForwardingHistoryRequest - (*ForwardingEvent)(nil), // 194: lnrpc.ForwardingEvent - (*ForwardingHistoryResponse)(nil), // 195: lnrpc.ForwardingHistoryResponse - (*ExportChannelBackupRequest)(nil), // 196: lnrpc.ExportChannelBackupRequest - (*ChannelBackup)(nil), // 197: lnrpc.ChannelBackup - (*MultiChanBackup)(nil), // 198: lnrpc.MultiChanBackup - (*ChanBackupExportRequest)(nil), // 199: lnrpc.ChanBackupExportRequest - (*ChanBackupSnapshot)(nil), // 200: lnrpc.ChanBackupSnapshot - (*ChannelBackups)(nil), // 201: lnrpc.ChannelBackups - (*RestoreChanBackupRequest)(nil), // 202: lnrpc.RestoreChanBackupRequest - (*RestoreBackupResponse)(nil), // 203: lnrpc.RestoreBackupResponse - (*ChannelBackupSubscription)(nil), // 204: lnrpc.ChannelBackupSubscription - (*VerifyChanBackupResponse)(nil), // 205: lnrpc.VerifyChanBackupResponse - (*MacaroonPermission)(nil), // 206: lnrpc.MacaroonPermission - (*BakeMacaroonRequest)(nil), // 207: lnrpc.BakeMacaroonRequest - (*BakeMacaroonResponse)(nil), // 208: lnrpc.BakeMacaroonResponse - (*ListMacaroonIDsRequest)(nil), // 209: lnrpc.ListMacaroonIDsRequest - (*ListMacaroonIDsResponse)(nil), // 210: lnrpc.ListMacaroonIDsResponse - (*DeleteMacaroonIDRequest)(nil), // 211: lnrpc.DeleteMacaroonIDRequest - (*DeleteMacaroonIDResponse)(nil), // 212: lnrpc.DeleteMacaroonIDResponse - (*MacaroonPermissionList)(nil), // 213: lnrpc.MacaroonPermissionList - (*ListPermissionsRequest)(nil), // 214: lnrpc.ListPermissionsRequest - (*ListPermissionsResponse)(nil), // 215: lnrpc.ListPermissionsResponse - (*Failure)(nil), // 216: lnrpc.Failure - (*ChannelUpdate)(nil), // 217: lnrpc.ChannelUpdate - (*MacaroonId)(nil), // 218: lnrpc.MacaroonId - (*Op)(nil), // 219: lnrpc.Op - (*CheckMacPermRequest)(nil), // 220: lnrpc.CheckMacPermRequest - (*CheckMacPermResponse)(nil), // 221: lnrpc.CheckMacPermResponse - (*RPCMiddlewareRequest)(nil), // 222: lnrpc.RPCMiddlewareRequest - (*MetadataValues)(nil), // 223: lnrpc.MetadataValues - (*StreamAuth)(nil), // 224: lnrpc.StreamAuth - (*RPCMessage)(nil), // 225: lnrpc.RPCMessage - (*RPCMiddlewareResponse)(nil), // 226: lnrpc.RPCMiddlewareResponse - (*MiddlewareRegistration)(nil), // 227: lnrpc.MiddlewareRegistration - (*InterceptFeedback)(nil), // 228: lnrpc.InterceptFeedback - nil, // 229: lnrpc.SendRequest.DestCustomRecordsEntry - nil, // 230: lnrpc.EstimateFeeRequest.AddrToAmountEntry - nil, // 231: lnrpc.SendManyRequest.AddrToAmountEntry - nil, // 232: lnrpc.Peer.FeaturesEntry - nil, // 233: lnrpc.GetInfoResponse.FeaturesEntry - nil, // 234: lnrpc.GetDebugInfoResponse.ConfigEntry - (*PendingChannelsResponse_PendingChannel)(nil), // 235: lnrpc.PendingChannelsResponse.PendingChannel - (*PendingChannelsResponse_PendingOpenChannel)(nil), // 236: lnrpc.PendingChannelsResponse.PendingOpenChannel - (*PendingChannelsResponse_WaitingCloseChannel)(nil), // 237: lnrpc.PendingChannelsResponse.WaitingCloseChannel - (*PendingChannelsResponse_Commitments)(nil), // 238: lnrpc.PendingChannelsResponse.Commitments - (*PendingChannelsResponse_ClosedChannel)(nil), // 239: lnrpc.PendingChannelsResponse.ClosedChannel - (*PendingChannelsResponse_ForceClosedChannel)(nil), // 240: lnrpc.PendingChannelsResponse.ForceClosedChannel - nil, // 241: lnrpc.WalletBalanceResponse.AccountBalanceEntry - nil, // 242: lnrpc.QueryRoutesRequest.DestCustomRecordsEntry - nil, // 243: lnrpc.Hop.CustomRecordsEntry - nil, // 244: lnrpc.LightningNode.FeaturesEntry - nil, // 245: lnrpc.LightningNode.CustomRecordsEntry - nil, // 246: lnrpc.RoutingPolicy.CustomRecordsEntry - nil, // 247: lnrpc.ChannelEdge.CustomRecordsEntry - nil, // 248: lnrpc.NodeMetricsResponse.BetweennessCentralityEntry - nil, // 249: lnrpc.NodeUpdate.FeaturesEntry - nil, // 250: lnrpc.Invoice.FeaturesEntry - nil, // 251: lnrpc.Invoice.AmpInvoiceStateEntry - nil, // 252: lnrpc.InvoiceHTLC.CustomRecordsEntry - nil, // 253: lnrpc.Payment.FirstHopCustomRecordsEntry - nil, // 254: lnrpc.PayReq.FeaturesEntry - nil, // 255: lnrpc.ListPermissionsResponse.MethodPermissionsEntry - nil, // 256: lnrpc.RPCMiddlewareRequest.MetadataPairsEntry + (*ListPaymentDuplicatesRequest)(nil), // 175: lnrpc.ListPaymentDuplicatesRequest + (*ListPaymentDuplicatesResponse)(nil), // 176: lnrpc.ListPaymentDuplicatesResponse + (*ListAllPaymentDuplicatesRequest)(nil), // 177: lnrpc.ListAllPaymentDuplicatesRequest + (*ListAllPaymentDuplicatesResponse)(nil), // 178: lnrpc.ListAllPaymentDuplicatesResponse + (*PaymentDuplicate)(nil), // 179: lnrpc.PaymentDuplicate + (*DeletePaymentRequest)(nil), // 180: lnrpc.DeletePaymentRequest + (*DeleteAllPaymentsRequest)(nil), // 181: lnrpc.DeleteAllPaymentsRequest + (*DeletePaymentResponse)(nil), // 182: lnrpc.DeletePaymentResponse + (*DeleteAllPaymentsResponse)(nil), // 183: lnrpc.DeleteAllPaymentsResponse + (*AbandonChannelRequest)(nil), // 184: lnrpc.AbandonChannelRequest + (*AbandonChannelResponse)(nil), // 185: lnrpc.AbandonChannelResponse + (*DebugLevelRequest)(nil), // 186: lnrpc.DebugLevelRequest + (*DebugLevelResponse)(nil), // 187: lnrpc.DebugLevelResponse + (*PayReqString)(nil), // 188: lnrpc.PayReqString + (*PayReq)(nil), // 189: lnrpc.PayReq + (*Feature)(nil), // 190: lnrpc.Feature + (*FeeReportRequest)(nil), // 191: lnrpc.FeeReportRequest + (*ChannelFeeReport)(nil), // 192: lnrpc.ChannelFeeReport + (*FeeReportResponse)(nil), // 193: lnrpc.FeeReportResponse + (*InboundFee)(nil), // 194: lnrpc.InboundFee + (*PolicyUpdateRequest)(nil), // 195: lnrpc.PolicyUpdateRequest + (*FailedUpdate)(nil), // 196: lnrpc.FailedUpdate + (*PolicyUpdateResponse)(nil), // 197: lnrpc.PolicyUpdateResponse + (*ForwardingHistoryRequest)(nil), // 198: lnrpc.ForwardingHistoryRequest + (*ForwardingEvent)(nil), // 199: lnrpc.ForwardingEvent + (*ForwardingHistoryResponse)(nil), // 200: lnrpc.ForwardingHistoryResponse + (*ExportChannelBackupRequest)(nil), // 201: lnrpc.ExportChannelBackupRequest + (*ChannelBackup)(nil), // 202: lnrpc.ChannelBackup + (*MultiChanBackup)(nil), // 203: lnrpc.MultiChanBackup + (*ChanBackupExportRequest)(nil), // 204: lnrpc.ChanBackupExportRequest + (*ChanBackupSnapshot)(nil), // 205: lnrpc.ChanBackupSnapshot + (*ChannelBackups)(nil), // 206: lnrpc.ChannelBackups + (*RestoreChanBackupRequest)(nil), // 207: lnrpc.RestoreChanBackupRequest + (*RestoreBackupResponse)(nil), // 208: lnrpc.RestoreBackupResponse + (*ChannelBackupSubscription)(nil), // 209: lnrpc.ChannelBackupSubscription + (*VerifyChanBackupResponse)(nil), // 210: lnrpc.VerifyChanBackupResponse + (*MacaroonPermission)(nil), // 211: lnrpc.MacaroonPermission + (*BakeMacaroonRequest)(nil), // 212: lnrpc.BakeMacaroonRequest + (*BakeMacaroonResponse)(nil), // 213: lnrpc.BakeMacaroonResponse + (*ListMacaroonIDsRequest)(nil), // 214: lnrpc.ListMacaroonIDsRequest + (*ListMacaroonIDsResponse)(nil), // 215: lnrpc.ListMacaroonIDsResponse + (*DeleteMacaroonIDRequest)(nil), // 216: lnrpc.DeleteMacaroonIDRequest + (*DeleteMacaroonIDResponse)(nil), // 217: lnrpc.DeleteMacaroonIDResponse + (*MacaroonPermissionList)(nil), // 218: lnrpc.MacaroonPermissionList + (*ListPermissionsRequest)(nil), // 219: lnrpc.ListPermissionsRequest + (*ListPermissionsResponse)(nil), // 220: lnrpc.ListPermissionsResponse + (*Failure)(nil), // 221: lnrpc.Failure + (*ChannelUpdate)(nil), // 222: lnrpc.ChannelUpdate + (*MacaroonId)(nil), // 223: lnrpc.MacaroonId + (*Op)(nil), // 224: lnrpc.Op + (*CheckMacPermRequest)(nil), // 225: lnrpc.CheckMacPermRequest + (*CheckMacPermResponse)(nil), // 226: lnrpc.CheckMacPermResponse + (*RPCMiddlewareRequest)(nil), // 227: lnrpc.RPCMiddlewareRequest + (*MetadataValues)(nil), // 228: lnrpc.MetadataValues + (*StreamAuth)(nil), // 229: lnrpc.StreamAuth + (*RPCMessage)(nil), // 230: lnrpc.RPCMessage + (*RPCMiddlewareResponse)(nil), // 231: lnrpc.RPCMiddlewareResponse + (*MiddlewareRegistration)(nil), // 232: lnrpc.MiddlewareRegistration + (*InterceptFeedback)(nil), // 233: lnrpc.InterceptFeedback + nil, // 234: lnrpc.SendRequest.DestCustomRecordsEntry + nil, // 235: lnrpc.EstimateFeeRequest.AddrToAmountEntry + nil, // 236: lnrpc.SendManyRequest.AddrToAmountEntry + nil, // 237: lnrpc.Peer.FeaturesEntry + nil, // 238: lnrpc.GetInfoResponse.FeaturesEntry + nil, // 239: lnrpc.GetDebugInfoResponse.ConfigEntry + (*PendingChannelsResponse_PendingChannel)(nil), // 240: lnrpc.PendingChannelsResponse.PendingChannel + (*PendingChannelsResponse_PendingOpenChannel)(nil), // 241: lnrpc.PendingChannelsResponse.PendingOpenChannel + (*PendingChannelsResponse_WaitingCloseChannel)(nil), // 242: lnrpc.PendingChannelsResponse.WaitingCloseChannel + (*PendingChannelsResponse_Commitments)(nil), // 243: lnrpc.PendingChannelsResponse.Commitments + (*PendingChannelsResponse_ClosedChannel)(nil), // 244: lnrpc.PendingChannelsResponse.ClosedChannel + (*PendingChannelsResponse_ForceClosedChannel)(nil), // 245: lnrpc.PendingChannelsResponse.ForceClosedChannel + nil, // 246: lnrpc.WalletBalanceResponse.AccountBalanceEntry + nil, // 247: lnrpc.QueryRoutesRequest.DestCustomRecordsEntry + nil, // 248: lnrpc.Hop.CustomRecordsEntry + nil, // 249: lnrpc.LightningNode.FeaturesEntry + nil, // 250: lnrpc.LightningNode.CustomRecordsEntry + nil, // 251: lnrpc.RoutingPolicy.CustomRecordsEntry + nil, // 252: lnrpc.ChannelEdge.CustomRecordsEntry + nil, // 253: lnrpc.NodeMetricsResponse.BetweennessCentralityEntry + nil, // 254: lnrpc.NodeUpdate.FeaturesEntry + nil, // 255: lnrpc.Invoice.FeaturesEntry + nil, // 256: lnrpc.Invoice.AmpInvoiceStateEntry + nil, // 257: lnrpc.InvoiceHTLC.CustomRecordsEntry + nil, // 258: lnrpc.Payment.FirstHopCustomRecordsEntry + nil, // 259: lnrpc.PayReq.FeaturesEntry + nil, // 260: lnrpc.ListPermissionsResponse.MethodPermissionsEntry + nil, // 261: lnrpc.RPCMiddlewareRequest.MetadataPairsEntry } var file_lightning_proto_depIdxs = []int32{ 2, // 0: lnrpc.Utxo.address_type:type_name -> lnrpc.AddressType @@ -22753,16 +23081,16 @@ var file_lightning_proto_depIdxs = []int32{ 44, // 4: lnrpc.Transaction.previous_outpoints:type_name -> lnrpc.PreviousOutPoint 33, // 5: lnrpc.TransactionDetails.transactions:type_name -> lnrpc.Transaction 36, // 6: lnrpc.SendRequest.fee_limit:type_name -> lnrpc.FeeLimit - 229, // 7: lnrpc.SendRequest.dest_custom_records:type_name -> lnrpc.SendRequest.DestCustomRecordsEntry + 234, // 7: lnrpc.SendRequest.dest_custom_records:type_name -> lnrpc.SendRequest.DestCustomRecordsEntry 10, // 8: lnrpc.SendRequest.dest_features:type_name -> lnrpc.FeatureBit 130, // 9: lnrpc.SendResponse.payment_route:type_name -> lnrpc.Route 130, // 10: lnrpc.SendToRouteRequest.route:type_name -> lnrpc.Route 3, // 11: lnrpc.ChannelAcceptRequest.commitment_type:type_name -> lnrpc.CommitmentType - 230, // 12: lnrpc.EstimateFeeRequest.AddrToAmount:type_name -> lnrpc.EstimateFeeRequest.AddrToAmountEntry + 235, // 12: lnrpc.EstimateFeeRequest.AddrToAmount:type_name -> lnrpc.EstimateFeeRequest.AddrToAmountEntry 1, // 13: lnrpc.EstimateFeeRequest.coin_selection_strategy:type_name -> lnrpc.CoinSelectionStrategy 43, // 14: lnrpc.EstimateFeeRequest.inputs:type_name -> lnrpc.OutPoint 43, // 15: lnrpc.EstimateFeeResponse.inputs:type_name -> lnrpc.OutPoint - 231, // 16: lnrpc.SendManyRequest.AddrToAmount:type_name -> lnrpc.SendManyRequest.AddrToAmountEntry + 236, // 16: lnrpc.SendManyRequest.AddrToAmount:type_name -> lnrpc.SendManyRequest.AddrToAmountEntry 1, // 17: lnrpc.SendManyRequest.coin_selection_strategy:type_name -> lnrpc.CoinSelectionStrategy 1, // 18: lnrpc.SendCoinsRequest.coin_selection_strategy:type_name -> lnrpc.CoinSelectionStrategy 43, // 19: lnrpc.SendCoinsRequest.outpoints:type_name -> lnrpc.OutPoint @@ -22784,13 +23112,13 @@ var file_lightning_proto_depIdxs = []int32{ 43, // 35: lnrpc.Resolution.outpoint:type_name -> lnrpc.OutPoint 72, // 36: lnrpc.ClosedChannelsResponse.channels:type_name -> lnrpc.ChannelCloseSummary 13, // 37: lnrpc.Peer.sync_type:type_name -> lnrpc.Peer.SyncType - 232, // 38: lnrpc.Peer.features:type_name -> lnrpc.Peer.FeaturesEntry + 237, // 38: lnrpc.Peer.features:type_name -> lnrpc.Peer.FeaturesEntry 77, // 39: lnrpc.Peer.errors:type_name -> lnrpc.TimestampedError 76, // 40: lnrpc.ListPeersResponse.peers:type_name -> lnrpc.Peer 14, // 41: lnrpc.PeerEvent.type:type_name -> lnrpc.PeerEvent.EventType 88, // 42: lnrpc.GetInfoResponse.chains:type_name -> lnrpc.Chain - 233, // 43: lnrpc.GetInfoResponse.features:type_name -> lnrpc.GetInfoResponse.FeaturesEntry - 234, // 44: lnrpc.GetDebugInfoResponse.config:type_name -> lnrpc.GetDebugInfoResponse.ConfigEntry + 238, // 43: lnrpc.GetInfoResponse.features:type_name -> lnrpc.GetInfoResponse.FeaturesEntry + 239, // 44: lnrpc.GetDebugInfoResponse.config:type_name -> lnrpc.GetDebugInfoResponse.ConfigEntry 42, // 45: lnrpc.ChannelOpenUpdate.channel_point:type_name -> lnrpc.ChannelPoint 90, // 46: lnrpc.ChannelCloseUpdate.local_close_output:type_name -> lnrpc.CloseOutput 90, // 47: lnrpc.ChannelCloseUpdate.remote_close_output:type_name -> lnrpc.CloseOutput @@ -22818,10 +23146,10 @@ var file_lightning_proto_depIdxs = []int32{ 107, // 69: lnrpc.FundingTransitionMsg.shim_cancel:type_name -> lnrpc.FundingShimCancel 108, // 70: lnrpc.FundingTransitionMsg.psbt_verify:type_name -> lnrpc.FundingPsbtVerify 109, // 71: lnrpc.FundingTransitionMsg.psbt_finalize:type_name -> lnrpc.FundingPsbtFinalize - 236, // 72: lnrpc.PendingChannelsResponse.pending_open_channels:type_name -> lnrpc.PendingChannelsResponse.PendingOpenChannel - 239, // 73: lnrpc.PendingChannelsResponse.pending_closing_channels:type_name -> lnrpc.PendingChannelsResponse.ClosedChannel - 240, // 74: lnrpc.PendingChannelsResponse.pending_force_closing_channels:type_name -> lnrpc.PendingChannelsResponse.ForceClosedChannel - 237, // 75: lnrpc.PendingChannelsResponse.waiting_close_channels:type_name -> lnrpc.PendingChannelsResponse.WaitingCloseChannel + 241, // 72: lnrpc.PendingChannelsResponse.pending_open_channels:type_name -> lnrpc.PendingChannelsResponse.PendingOpenChannel + 244, // 73: lnrpc.PendingChannelsResponse.pending_closing_channels:type_name -> lnrpc.PendingChannelsResponse.ClosedChannel + 245, // 74: lnrpc.PendingChannelsResponse.pending_force_closing_channels:type_name -> lnrpc.PendingChannelsResponse.ForceClosedChannel + 242, // 75: lnrpc.PendingChannelsResponse.waiting_close_channels:type_name -> lnrpc.PendingChannelsResponse.WaitingCloseChannel 66, // 76: lnrpc.ChannelEventUpdate.open_channel:type_name -> lnrpc.Channel 72, // 77: lnrpc.ChannelEventUpdate.closed_channel:type_name -> lnrpc.ChannelCloseSummary 42, // 78: lnrpc.ChannelEventUpdate.active_channel:type_name -> lnrpc.ChannelPoint @@ -22830,7 +23158,7 @@ var file_lightning_proto_depIdxs = []int32{ 42, // 81: lnrpc.ChannelEventUpdate.fully_resolved_channel:type_name -> lnrpc.ChannelPoint 42, // 82: lnrpc.ChannelEventUpdate.channel_funding_timeout:type_name -> lnrpc.ChannelPoint 16, // 83: lnrpc.ChannelEventUpdate.type:type_name -> lnrpc.ChannelEventUpdate.UpdateType - 241, // 84: lnrpc.WalletBalanceResponse.account_balance:type_name -> lnrpc.WalletBalanceResponse.AccountBalanceEntry + 246, // 84: lnrpc.WalletBalanceResponse.account_balance:type_name -> lnrpc.WalletBalanceResponse.AccountBalanceEntry 120, // 85: lnrpc.ChannelBalanceResponse.local_balance:type_name -> lnrpc.Amount 120, // 86: lnrpc.ChannelBalanceResponse.remote_balance:type_name -> lnrpc.Amount 120, // 87: lnrpc.ChannelBalanceResponse.unsettled_local_balance:type_name -> lnrpc.Amount @@ -22840,34 +23168,34 @@ var file_lightning_proto_depIdxs = []int32{ 36, // 91: lnrpc.QueryRoutesRequest.fee_limit:type_name -> lnrpc.FeeLimit 125, // 92: lnrpc.QueryRoutesRequest.ignored_edges:type_name -> lnrpc.EdgeLocator 124, // 93: lnrpc.QueryRoutesRequest.ignored_pairs:type_name -> lnrpc.NodePair - 242, // 94: lnrpc.QueryRoutesRequest.dest_custom_records:type_name -> lnrpc.QueryRoutesRequest.DestCustomRecordsEntry + 247, // 94: lnrpc.QueryRoutesRequest.dest_custom_records:type_name -> lnrpc.QueryRoutesRequest.DestCustomRecordsEntry 155, // 95: lnrpc.QueryRoutesRequest.route_hints:type_name -> lnrpc.RouteHint 156, // 96: lnrpc.QueryRoutesRequest.blinded_payment_paths:type_name -> lnrpc.BlindedPaymentPath 10, // 97: lnrpc.QueryRoutesRequest.dest_features:type_name -> lnrpc.FeatureBit 130, // 98: lnrpc.QueryRoutesResponse.routes:type_name -> lnrpc.Route 128, // 99: lnrpc.Hop.mpp_record:type_name -> lnrpc.MPPRecord 129, // 100: lnrpc.Hop.amp_record:type_name -> lnrpc.AMPRecord - 243, // 101: lnrpc.Hop.custom_records:type_name -> lnrpc.Hop.CustomRecordsEntry + 248, // 101: lnrpc.Hop.custom_records:type_name -> lnrpc.Hop.CustomRecordsEntry 127, // 102: lnrpc.Route.hops:type_name -> lnrpc.Hop 133, // 103: lnrpc.NodeInfo.node:type_name -> lnrpc.LightningNode 137, // 104: lnrpc.NodeInfo.channels:type_name -> lnrpc.ChannelEdge 134, // 105: lnrpc.LightningNode.addresses:type_name -> lnrpc.NodeAddress - 244, // 106: lnrpc.LightningNode.features:type_name -> lnrpc.LightningNode.FeaturesEntry - 245, // 107: lnrpc.LightningNode.custom_records:type_name -> lnrpc.LightningNode.CustomRecordsEntry - 246, // 108: lnrpc.RoutingPolicy.custom_records:type_name -> lnrpc.RoutingPolicy.CustomRecordsEntry + 249, // 106: lnrpc.LightningNode.features:type_name -> lnrpc.LightningNode.FeaturesEntry + 250, // 107: lnrpc.LightningNode.custom_records:type_name -> lnrpc.LightningNode.CustomRecordsEntry + 251, // 108: lnrpc.RoutingPolicy.custom_records:type_name -> lnrpc.RoutingPolicy.CustomRecordsEntry 135, // 109: lnrpc.ChannelEdge.node1_policy:type_name -> lnrpc.RoutingPolicy 135, // 110: lnrpc.ChannelEdge.node2_policy:type_name -> lnrpc.RoutingPolicy - 247, // 111: lnrpc.ChannelEdge.custom_records:type_name -> lnrpc.ChannelEdge.CustomRecordsEntry + 252, // 111: lnrpc.ChannelEdge.custom_records:type_name -> lnrpc.ChannelEdge.CustomRecordsEntry 136, // 112: lnrpc.ChannelEdge.auth_proof:type_name -> lnrpc.ChannelAuthProof 133, // 113: lnrpc.ChannelGraph.nodes:type_name -> lnrpc.LightningNode 137, // 114: lnrpc.ChannelGraph.edges:type_name -> lnrpc.ChannelEdge 7, // 115: lnrpc.NodeMetricsRequest.types:type_name -> lnrpc.NodeMetricType - 248, // 116: lnrpc.NodeMetricsResponse.betweenness_centrality:type_name -> lnrpc.NodeMetricsResponse.BetweennessCentralityEntry + 253, // 116: lnrpc.NodeMetricsResponse.betweenness_centrality:type_name -> lnrpc.NodeMetricsResponse.BetweennessCentralityEntry 150, // 117: lnrpc.GraphTopologyUpdate.node_updates:type_name -> lnrpc.NodeUpdate 151, // 118: lnrpc.GraphTopologyUpdate.channel_updates:type_name -> lnrpc.ChannelEdgeUpdate 152, // 119: lnrpc.GraphTopologyUpdate.closed_chans:type_name -> lnrpc.ClosedChannelUpdate 134, // 120: lnrpc.NodeUpdate.node_addresses:type_name -> lnrpc.NodeAddress - 249, // 121: lnrpc.NodeUpdate.features:type_name -> lnrpc.NodeUpdate.FeaturesEntry + 254, // 121: lnrpc.NodeUpdate.features:type_name -> lnrpc.NodeUpdate.FeaturesEntry 42, // 122: lnrpc.ChannelEdgeUpdate.chan_point:type_name -> lnrpc.ChannelPoint 135, // 123: lnrpc.ChannelEdgeUpdate.routing_policy:type_name -> lnrpc.RoutingPolicy 42, // 124: lnrpc.ClosedChannelUpdate.chan_point:type_name -> lnrpc.ChannelPoint @@ -22879,219 +23207,226 @@ var file_lightning_proto_depIdxs = []int32{ 155, // 130: lnrpc.Invoice.route_hints:type_name -> lnrpc.RouteHint 17, // 131: lnrpc.Invoice.state:type_name -> lnrpc.Invoice.InvoiceState 162, // 132: lnrpc.Invoice.htlcs:type_name -> lnrpc.InvoiceHTLC - 250, // 133: lnrpc.Invoice.features:type_name -> lnrpc.Invoice.FeaturesEntry - 251, // 134: lnrpc.Invoice.amp_invoice_state:type_name -> lnrpc.Invoice.AmpInvoiceStateEntry + 255, // 133: lnrpc.Invoice.features:type_name -> lnrpc.Invoice.FeaturesEntry + 256, // 134: lnrpc.Invoice.amp_invoice_state:type_name -> lnrpc.Invoice.AmpInvoiceStateEntry 161, // 135: lnrpc.Invoice.blinded_path_config:type_name -> lnrpc.BlindedPathConfig 8, // 136: lnrpc.InvoiceHTLC.state:type_name -> lnrpc.InvoiceHTLCState - 252, // 137: lnrpc.InvoiceHTLC.custom_records:type_name -> lnrpc.InvoiceHTLC.CustomRecordsEntry + 257, // 137: lnrpc.InvoiceHTLC.custom_records:type_name -> lnrpc.InvoiceHTLC.CustomRecordsEntry 163, // 138: lnrpc.InvoiceHTLC.amp:type_name -> lnrpc.AMP 160, // 139: lnrpc.ListInvoiceResponse.invoices:type_name -> lnrpc.Invoice 18, // 140: lnrpc.Payment.status:type_name -> lnrpc.Payment.PaymentStatus 172, // 141: lnrpc.Payment.htlcs:type_name -> lnrpc.HTLCAttempt 9, // 142: lnrpc.Payment.failure_reason:type_name -> lnrpc.PaymentFailureReason - 253, // 143: lnrpc.Payment.first_hop_custom_records:type_name -> lnrpc.Payment.FirstHopCustomRecordsEntry + 258, // 143: lnrpc.Payment.first_hop_custom_records:type_name -> lnrpc.Payment.FirstHopCustomRecordsEntry 19, // 144: lnrpc.HTLCAttempt.status:type_name -> lnrpc.HTLCAttempt.HTLCStatus 130, // 145: lnrpc.HTLCAttempt.route:type_name -> lnrpc.Route - 216, // 146: lnrpc.HTLCAttempt.failure:type_name -> lnrpc.Failure + 221, // 146: lnrpc.HTLCAttempt.failure:type_name -> lnrpc.Failure 171, // 147: lnrpc.ListPaymentsResponse.payments:type_name -> lnrpc.Payment - 42, // 148: lnrpc.AbandonChannelRequest.channel_point:type_name -> lnrpc.ChannelPoint - 155, // 149: lnrpc.PayReq.route_hints:type_name -> lnrpc.RouteHint - 254, // 150: lnrpc.PayReq.features:type_name -> lnrpc.PayReq.FeaturesEntry - 156, // 151: lnrpc.PayReq.blinded_paths:type_name -> lnrpc.BlindedPaymentPath - 187, // 152: lnrpc.FeeReportResponse.channel_fees:type_name -> lnrpc.ChannelFeeReport - 42, // 153: lnrpc.PolicyUpdateRequest.chan_point:type_name -> lnrpc.ChannelPoint - 189, // 154: lnrpc.PolicyUpdateRequest.inbound_fee:type_name -> lnrpc.InboundFee - 43, // 155: lnrpc.FailedUpdate.outpoint:type_name -> lnrpc.OutPoint - 11, // 156: lnrpc.FailedUpdate.reason:type_name -> lnrpc.UpdateFailure - 191, // 157: lnrpc.PolicyUpdateResponse.failed_updates:type_name -> lnrpc.FailedUpdate - 194, // 158: lnrpc.ForwardingHistoryResponse.forwarding_events:type_name -> lnrpc.ForwardingEvent - 42, // 159: lnrpc.ExportChannelBackupRequest.chan_point:type_name -> lnrpc.ChannelPoint - 42, // 160: lnrpc.ChannelBackup.chan_point:type_name -> lnrpc.ChannelPoint - 42, // 161: lnrpc.MultiChanBackup.chan_points:type_name -> lnrpc.ChannelPoint - 201, // 162: lnrpc.ChanBackupSnapshot.single_chan_backups:type_name -> lnrpc.ChannelBackups - 198, // 163: lnrpc.ChanBackupSnapshot.multi_chan_backup:type_name -> lnrpc.MultiChanBackup - 197, // 164: lnrpc.ChannelBackups.chan_backups:type_name -> lnrpc.ChannelBackup - 201, // 165: lnrpc.RestoreChanBackupRequest.chan_backups:type_name -> lnrpc.ChannelBackups - 206, // 166: lnrpc.BakeMacaroonRequest.permissions:type_name -> lnrpc.MacaroonPermission - 206, // 167: lnrpc.MacaroonPermissionList.permissions:type_name -> lnrpc.MacaroonPermission - 255, // 168: lnrpc.ListPermissionsResponse.method_permissions:type_name -> lnrpc.ListPermissionsResponse.MethodPermissionsEntry - 20, // 169: lnrpc.Failure.code:type_name -> lnrpc.Failure.FailureCode - 217, // 170: lnrpc.Failure.channel_update:type_name -> lnrpc.ChannelUpdate - 219, // 171: lnrpc.MacaroonId.ops:type_name -> lnrpc.Op - 206, // 172: lnrpc.CheckMacPermRequest.permissions:type_name -> lnrpc.MacaroonPermission - 224, // 173: lnrpc.RPCMiddlewareRequest.stream_auth:type_name -> lnrpc.StreamAuth - 225, // 174: lnrpc.RPCMiddlewareRequest.request:type_name -> lnrpc.RPCMessage - 225, // 175: lnrpc.RPCMiddlewareRequest.response:type_name -> lnrpc.RPCMessage - 256, // 176: lnrpc.RPCMiddlewareRequest.metadata_pairs:type_name -> lnrpc.RPCMiddlewareRequest.MetadataPairsEntry - 227, // 177: lnrpc.RPCMiddlewareResponse.register:type_name -> lnrpc.MiddlewareRegistration - 228, // 178: lnrpc.RPCMiddlewareResponse.feedback:type_name -> lnrpc.InterceptFeedback - 185, // 179: lnrpc.Peer.FeaturesEntry.value:type_name -> lnrpc.Feature - 185, // 180: lnrpc.GetInfoResponse.FeaturesEntry.value:type_name -> lnrpc.Feature - 4, // 181: lnrpc.PendingChannelsResponse.PendingChannel.initiator:type_name -> lnrpc.Initiator - 3, // 182: lnrpc.PendingChannelsResponse.PendingChannel.commitment_type:type_name -> lnrpc.CommitmentType - 235, // 183: lnrpc.PendingChannelsResponse.PendingOpenChannel.channel:type_name -> lnrpc.PendingChannelsResponse.PendingChannel - 235, // 184: lnrpc.PendingChannelsResponse.WaitingCloseChannel.channel:type_name -> lnrpc.PendingChannelsResponse.PendingChannel - 238, // 185: lnrpc.PendingChannelsResponse.WaitingCloseChannel.commitments:type_name -> lnrpc.PendingChannelsResponse.Commitments - 235, // 186: lnrpc.PendingChannelsResponse.ClosedChannel.channel:type_name -> lnrpc.PendingChannelsResponse.PendingChannel - 235, // 187: lnrpc.PendingChannelsResponse.ForceClosedChannel.channel:type_name -> lnrpc.PendingChannelsResponse.PendingChannel - 112, // 188: lnrpc.PendingChannelsResponse.ForceClosedChannel.pending_htlcs:type_name -> lnrpc.PendingHTLC - 15, // 189: lnrpc.PendingChannelsResponse.ForceClosedChannel.anchor:type_name -> lnrpc.PendingChannelsResponse.ForceClosedChannel.AnchorState - 117, // 190: lnrpc.WalletBalanceResponse.AccountBalanceEntry.value:type_name -> lnrpc.WalletAccountBalance - 185, // 191: lnrpc.LightningNode.FeaturesEntry.value:type_name -> lnrpc.Feature - 142, // 192: lnrpc.NodeMetricsResponse.BetweennessCentralityEntry.value:type_name -> lnrpc.FloatMetric - 185, // 193: lnrpc.NodeUpdate.FeaturesEntry.value:type_name -> lnrpc.Feature - 185, // 194: lnrpc.Invoice.FeaturesEntry.value:type_name -> lnrpc.Feature - 159, // 195: lnrpc.Invoice.AmpInvoiceStateEntry.value:type_name -> lnrpc.AMPInvoiceState - 185, // 196: lnrpc.PayReq.FeaturesEntry.value:type_name -> lnrpc.Feature - 213, // 197: lnrpc.ListPermissionsResponse.MethodPermissionsEntry.value:type_name -> lnrpc.MacaroonPermissionList - 223, // 198: lnrpc.RPCMiddlewareRequest.MetadataPairsEntry.value:type_name -> lnrpc.MetadataValues - 118, // 199: lnrpc.Lightning.WalletBalance:input_type -> lnrpc.WalletBalanceRequest - 121, // 200: lnrpc.Lightning.ChannelBalance:input_type -> lnrpc.ChannelBalanceRequest - 34, // 201: lnrpc.Lightning.GetTransactions:input_type -> lnrpc.GetTransactionsRequest - 46, // 202: lnrpc.Lightning.EstimateFee:input_type -> lnrpc.EstimateFeeRequest - 50, // 203: lnrpc.Lightning.SendCoins:input_type -> lnrpc.SendCoinsRequest - 52, // 204: lnrpc.Lightning.ListUnspent:input_type -> lnrpc.ListUnspentRequest - 34, // 205: lnrpc.Lightning.SubscribeTransactions:input_type -> lnrpc.GetTransactionsRequest - 48, // 206: lnrpc.Lightning.SendMany:input_type -> lnrpc.SendManyRequest - 54, // 207: lnrpc.Lightning.NewAddress:input_type -> lnrpc.NewAddressRequest - 56, // 208: lnrpc.Lightning.SignMessage:input_type -> lnrpc.SignMessageRequest - 58, // 209: lnrpc.Lightning.VerifyMessage:input_type -> lnrpc.VerifyMessageRequest - 60, // 210: lnrpc.Lightning.ConnectPeer:input_type -> lnrpc.ConnectPeerRequest - 62, // 211: lnrpc.Lightning.DisconnectPeer:input_type -> lnrpc.DisconnectPeerRequest - 78, // 212: lnrpc.Lightning.ListPeers:input_type -> lnrpc.ListPeersRequest - 80, // 213: lnrpc.Lightning.SubscribePeerEvents:input_type -> lnrpc.PeerEventSubscription - 82, // 214: lnrpc.Lightning.GetInfo:input_type -> lnrpc.GetInfoRequest - 84, // 215: lnrpc.Lightning.GetDebugInfo:input_type -> lnrpc.GetDebugInfoRequest - 86, // 216: lnrpc.Lightning.GetRecoveryInfo:input_type -> lnrpc.GetRecoveryInfoRequest - 113, // 217: lnrpc.Lightning.PendingChannels:input_type -> lnrpc.PendingChannelsRequest - 67, // 218: lnrpc.Lightning.ListChannels:input_type -> lnrpc.ListChannelsRequest - 115, // 219: lnrpc.Lightning.SubscribeChannelEvents:input_type -> lnrpc.ChannelEventSubscription - 74, // 220: lnrpc.Lightning.ClosedChannels:input_type -> lnrpc.ClosedChannelsRequest - 100, // 221: lnrpc.Lightning.OpenChannelSync:input_type -> lnrpc.OpenChannelRequest - 100, // 222: lnrpc.Lightning.OpenChannel:input_type -> lnrpc.OpenChannelRequest - 97, // 223: lnrpc.Lightning.BatchOpenChannel:input_type -> lnrpc.BatchOpenChannelRequest - 110, // 224: lnrpc.Lightning.FundingStateStep:input_type -> lnrpc.FundingTransitionMsg - 41, // 225: lnrpc.Lightning.ChannelAcceptor:input_type -> lnrpc.ChannelAcceptResponse - 92, // 226: lnrpc.Lightning.CloseChannel:input_type -> lnrpc.CloseChannelRequest - 179, // 227: lnrpc.Lightning.AbandonChannel:input_type -> lnrpc.AbandonChannelRequest - 37, // 228: lnrpc.Lightning.SendPayment:input_type -> lnrpc.SendRequest - 37, // 229: lnrpc.Lightning.SendPaymentSync:input_type -> lnrpc.SendRequest - 39, // 230: lnrpc.Lightning.SendToRoute:input_type -> lnrpc.SendToRouteRequest - 39, // 231: lnrpc.Lightning.SendToRouteSync:input_type -> lnrpc.SendToRouteRequest - 160, // 232: lnrpc.Lightning.AddInvoice:input_type -> lnrpc.Invoice - 166, // 233: lnrpc.Lightning.ListInvoices:input_type -> lnrpc.ListInvoiceRequest - 165, // 234: lnrpc.Lightning.LookupInvoice:input_type -> lnrpc.PaymentHash - 168, // 235: lnrpc.Lightning.SubscribeInvoices:input_type -> lnrpc.InvoiceSubscription - 169, // 236: lnrpc.Lightning.DeleteCanceledInvoice:input_type -> lnrpc.DelCanceledInvoiceReq - 183, // 237: lnrpc.Lightning.DecodePayReq:input_type -> lnrpc.PayReqString - 173, // 238: lnrpc.Lightning.ListPayments:input_type -> lnrpc.ListPaymentsRequest - 175, // 239: lnrpc.Lightning.DeletePayment:input_type -> lnrpc.DeletePaymentRequest - 176, // 240: lnrpc.Lightning.DeleteAllPayments:input_type -> lnrpc.DeleteAllPaymentsRequest - 138, // 241: lnrpc.Lightning.DescribeGraph:input_type -> lnrpc.ChannelGraphRequest - 140, // 242: lnrpc.Lightning.GetNodeMetrics:input_type -> lnrpc.NodeMetricsRequest - 143, // 243: lnrpc.Lightning.GetChanInfo:input_type -> lnrpc.ChanInfoRequest - 131, // 244: lnrpc.Lightning.GetNodeInfo:input_type -> lnrpc.NodeInfoRequest - 123, // 245: lnrpc.Lightning.QueryRoutes:input_type -> lnrpc.QueryRoutesRequest - 144, // 246: lnrpc.Lightning.GetNetworkInfo:input_type -> lnrpc.NetworkInfoRequest - 146, // 247: lnrpc.Lightning.StopDaemon:input_type -> lnrpc.StopRequest - 148, // 248: lnrpc.Lightning.SubscribeChannelGraph:input_type -> lnrpc.GraphTopologySubscription - 181, // 249: lnrpc.Lightning.DebugLevel:input_type -> lnrpc.DebugLevelRequest - 186, // 250: lnrpc.Lightning.FeeReport:input_type -> lnrpc.FeeReportRequest - 190, // 251: lnrpc.Lightning.UpdateChannelPolicy:input_type -> lnrpc.PolicyUpdateRequest - 193, // 252: lnrpc.Lightning.ForwardingHistory:input_type -> lnrpc.ForwardingHistoryRequest - 196, // 253: lnrpc.Lightning.ExportChannelBackup:input_type -> lnrpc.ExportChannelBackupRequest - 199, // 254: lnrpc.Lightning.ExportAllChannelBackups:input_type -> lnrpc.ChanBackupExportRequest - 200, // 255: lnrpc.Lightning.VerifyChanBackup:input_type -> lnrpc.ChanBackupSnapshot - 202, // 256: lnrpc.Lightning.RestoreChannelBackups:input_type -> lnrpc.RestoreChanBackupRequest - 204, // 257: lnrpc.Lightning.SubscribeChannelBackups:input_type -> lnrpc.ChannelBackupSubscription - 207, // 258: lnrpc.Lightning.BakeMacaroon:input_type -> lnrpc.BakeMacaroonRequest - 209, // 259: lnrpc.Lightning.ListMacaroonIDs:input_type -> lnrpc.ListMacaroonIDsRequest - 211, // 260: lnrpc.Lightning.DeleteMacaroonID:input_type -> lnrpc.DeleteMacaroonIDRequest - 214, // 261: lnrpc.Lightning.ListPermissions:input_type -> lnrpc.ListPermissionsRequest - 220, // 262: lnrpc.Lightning.CheckMacaroonPermissions:input_type -> lnrpc.CheckMacPermRequest - 226, // 263: lnrpc.Lightning.RegisterRPCMiddleware:input_type -> lnrpc.RPCMiddlewareResponse - 25, // 264: lnrpc.Lightning.SendCustomMessage:input_type -> lnrpc.SendCustomMessageRequest - 23, // 265: lnrpc.Lightning.SubscribeCustomMessages:input_type -> lnrpc.SubscribeCustomMessagesRequest - 29, // 266: lnrpc.Lightning.SendOnionMessage:input_type -> lnrpc.SendOnionMessageRequest - 27, // 267: lnrpc.Lightning.SubscribeOnionMessages:input_type -> lnrpc.SubscribeOnionMessagesRequest - 70, // 268: lnrpc.Lightning.ListAliases:input_type -> lnrpc.ListAliasesRequest - 21, // 269: lnrpc.Lightning.LookupHtlcResolution:input_type -> lnrpc.LookupHtlcResolutionRequest - 119, // 270: lnrpc.Lightning.WalletBalance:output_type -> lnrpc.WalletBalanceResponse - 122, // 271: lnrpc.Lightning.ChannelBalance:output_type -> lnrpc.ChannelBalanceResponse - 35, // 272: lnrpc.Lightning.GetTransactions:output_type -> lnrpc.TransactionDetails - 47, // 273: lnrpc.Lightning.EstimateFee:output_type -> lnrpc.EstimateFeeResponse - 51, // 274: lnrpc.Lightning.SendCoins:output_type -> lnrpc.SendCoinsResponse - 53, // 275: lnrpc.Lightning.ListUnspent:output_type -> lnrpc.ListUnspentResponse - 33, // 276: lnrpc.Lightning.SubscribeTransactions:output_type -> lnrpc.Transaction - 49, // 277: lnrpc.Lightning.SendMany:output_type -> lnrpc.SendManyResponse - 55, // 278: lnrpc.Lightning.NewAddress:output_type -> lnrpc.NewAddressResponse - 57, // 279: lnrpc.Lightning.SignMessage:output_type -> lnrpc.SignMessageResponse - 59, // 280: lnrpc.Lightning.VerifyMessage:output_type -> lnrpc.VerifyMessageResponse - 61, // 281: lnrpc.Lightning.ConnectPeer:output_type -> lnrpc.ConnectPeerResponse - 63, // 282: lnrpc.Lightning.DisconnectPeer:output_type -> lnrpc.DisconnectPeerResponse - 79, // 283: lnrpc.Lightning.ListPeers:output_type -> lnrpc.ListPeersResponse - 81, // 284: lnrpc.Lightning.SubscribePeerEvents:output_type -> lnrpc.PeerEvent - 83, // 285: lnrpc.Lightning.GetInfo:output_type -> lnrpc.GetInfoResponse - 85, // 286: lnrpc.Lightning.GetDebugInfo:output_type -> lnrpc.GetDebugInfoResponse - 87, // 287: lnrpc.Lightning.GetRecoveryInfo:output_type -> lnrpc.GetRecoveryInfoResponse - 114, // 288: lnrpc.Lightning.PendingChannels:output_type -> lnrpc.PendingChannelsResponse - 68, // 289: lnrpc.Lightning.ListChannels:output_type -> lnrpc.ListChannelsResponse - 116, // 290: lnrpc.Lightning.SubscribeChannelEvents:output_type -> lnrpc.ChannelEventUpdate - 75, // 291: lnrpc.Lightning.ClosedChannels:output_type -> lnrpc.ClosedChannelsResponse - 42, // 292: lnrpc.Lightning.OpenChannelSync:output_type -> lnrpc.ChannelPoint - 101, // 293: lnrpc.Lightning.OpenChannel:output_type -> lnrpc.OpenStatusUpdate - 99, // 294: lnrpc.Lightning.BatchOpenChannel:output_type -> lnrpc.BatchOpenChannelResponse - 111, // 295: lnrpc.Lightning.FundingStateStep:output_type -> lnrpc.FundingStateStepResp - 40, // 296: lnrpc.Lightning.ChannelAcceptor:output_type -> lnrpc.ChannelAcceptRequest - 93, // 297: lnrpc.Lightning.CloseChannel:output_type -> lnrpc.CloseStatusUpdate - 180, // 298: lnrpc.Lightning.AbandonChannel:output_type -> lnrpc.AbandonChannelResponse - 38, // 299: lnrpc.Lightning.SendPayment:output_type -> lnrpc.SendResponse - 38, // 300: lnrpc.Lightning.SendPaymentSync:output_type -> lnrpc.SendResponse - 38, // 301: lnrpc.Lightning.SendToRoute:output_type -> lnrpc.SendResponse - 38, // 302: lnrpc.Lightning.SendToRouteSync:output_type -> lnrpc.SendResponse - 164, // 303: lnrpc.Lightning.AddInvoice:output_type -> lnrpc.AddInvoiceResponse - 167, // 304: lnrpc.Lightning.ListInvoices:output_type -> lnrpc.ListInvoiceResponse - 160, // 305: lnrpc.Lightning.LookupInvoice:output_type -> lnrpc.Invoice - 160, // 306: lnrpc.Lightning.SubscribeInvoices:output_type -> lnrpc.Invoice - 170, // 307: lnrpc.Lightning.DeleteCanceledInvoice:output_type -> lnrpc.DelCanceledInvoiceResp - 184, // 308: lnrpc.Lightning.DecodePayReq:output_type -> lnrpc.PayReq - 174, // 309: lnrpc.Lightning.ListPayments:output_type -> lnrpc.ListPaymentsResponse - 177, // 310: lnrpc.Lightning.DeletePayment:output_type -> lnrpc.DeletePaymentResponse - 178, // 311: lnrpc.Lightning.DeleteAllPayments:output_type -> lnrpc.DeleteAllPaymentsResponse - 139, // 312: lnrpc.Lightning.DescribeGraph:output_type -> lnrpc.ChannelGraph - 141, // 313: lnrpc.Lightning.GetNodeMetrics:output_type -> lnrpc.NodeMetricsResponse - 137, // 314: lnrpc.Lightning.GetChanInfo:output_type -> lnrpc.ChannelEdge - 132, // 315: lnrpc.Lightning.GetNodeInfo:output_type -> lnrpc.NodeInfo - 126, // 316: lnrpc.Lightning.QueryRoutes:output_type -> lnrpc.QueryRoutesResponse - 145, // 317: lnrpc.Lightning.GetNetworkInfo:output_type -> lnrpc.NetworkInfo - 147, // 318: lnrpc.Lightning.StopDaemon:output_type -> lnrpc.StopResponse - 149, // 319: lnrpc.Lightning.SubscribeChannelGraph:output_type -> lnrpc.GraphTopologyUpdate - 182, // 320: lnrpc.Lightning.DebugLevel:output_type -> lnrpc.DebugLevelResponse - 188, // 321: lnrpc.Lightning.FeeReport:output_type -> lnrpc.FeeReportResponse - 192, // 322: lnrpc.Lightning.UpdateChannelPolicy:output_type -> lnrpc.PolicyUpdateResponse - 195, // 323: lnrpc.Lightning.ForwardingHistory:output_type -> lnrpc.ForwardingHistoryResponse - 197, // 324: lnrpc.Lightning.ExportChannelBackup:output_type -> lnrpc.ChannelBackup - 200, // 325: lnrpc.Lightning.ExportAllChannelBackups:output_type -> lnrpc.ChanBackupSnapshot - 205, // 326: lnrpc.Lightning.VerifyChanBackup:output_type -> lnrpc.VerifyChanBackupResponse - 203, // 327: lnrpc.Lightning.RestoreChannelBackups:output_type -> lnrpc.RestoreBackupResponse - 200, // 328: lnrpc.Lightning.SubscribeChannelBackups:output_type -> lnrpc.ChanBackupSnapshot - 208, // 329: lnrpc.Lightning.BakeMacaroon:output_type -> lnrpc.BakeMacaroonResponse - 210, // 330: lnrpc.Lightning.ListMacaroonIDs:output_type -> lnrpc.ListMacaroonIDsResponse - 212, // 331: lnrpc.Lightning.DeleteMacaroonID:output_type -> lnrpc.DeleteMacaroonIDResponse - 215, // 332: lnrpc.Lightning.ListPermissions:output_type -> lnrpc.ListPermissionsResponse - 221, // 333: lnrpc.Lightning.CheckMacaroonPermissions:output_type -> lnrpc.CheckMacPermResponse - 222, // 334: lnrpc.Lightning.RegisterRPCMiddleware:output_type -> lnrpc.RPCMiddlewareRequest - 26, // 335: lnrpc.Lightning.SendCustomMessage:output_type -> lnrpc.SendCustomMessageResponse - 24, // 336: lnrpc.Lightning.SubscribeCustomMessages:output_type -> lnrpc.CustomMessage - 30, // 337: lnrpc.Lightning.SendOnionMessage:output_type -> lnrpc.SendOnionMessageResponse - 28, // 338: lnrpc.Lightning.SubscribeOnionMessages:output_type -> lnrpc.OnionMessage - 71, // 339: lnrpc.Lightning.ListAliases:output_type -> lnrpc.ListAliasesResponse - 22, // 340: lnrpc.Lightning.LookupHtlcResolution:output_type -> lnrpc.LookupHtlcResolutionResponse - 270, // [270:341] is the sub-list for method output_type - 199, // [199:270] is the sub-list for method input_type - 199, // [199:199] is the sub-list for extension type_name - 199, // [199:199] is the sub-list for extension extendee - 0, // [0:199] is the sub-list for field type_name + 179, // 148: lnrpc.ListPaymentDuplicatesResponse.duplicates:type_name -> lnrpc.PaymentDuplicate + 179, // 149: lnrpc.ListAllPaymentDuplicatesResponse.duplicates:type_name -> lnrpc.PaymentDuplicate + 9, // 150: lnrpc.PaymentDuplicate.failure_reason:type_name -> lnrpc.PaymentFailureReason + 42, // 151: lnrpc.AbandonChannelRequest.channel_point:type_name -> lnrpc.ChannelPoint + 155, // 152: lnrpc.PayReq.route_hints:type_name -> lnrpc.RouteHint + 259, // 153: lnrpc.PayReq.features:type_name -> lnrpc.PayReq.FeaturesEntry + 156, // 154: lnrpc.PayReq.blinded_paths:type_name -> lnrpc.BlindedPaymentPath + 192, // 155: lnrpc.FeeReportResponse.channel_fees:type_name -> lnrpc.ChannelFeeReport + 42, // 156: lnrpc.PolicyUpdateRequest.chan_point:type_name -> lnrpc.ChannelPoint + 194, // 157: lnrpc.PolicyUpdateRequest.inbound_fee:type_name -> lnrpc.InboundFee + 43, // 158: lnrpc.FailedUpdate.outpoint:type_name -> lnrpc.OutPoint + 11, // 159: lnrpc.FailedUpdate.reason:type_name -> lnrpc.UpdateFailure + 196, // 160: lnrpc.PolicyUpdateResponse.failed_updates:type_name -> lnrpc.FailedUpdate + 199, // 161: lnrpc.ForwardingHistoryResponse.forwarding_events:type_name -> lnrpc.ForwardingEvent + 42, // 162: lnrpc.ExportChannelBackupRequest.chan_point:type_name -> lnrpc.ChannelPoint + 42, // 163: lnrpc.ChannelBackup.chan_point:type_name -> lnrpc.ChannelPoint + 42, // 164: lnrpc.MultiChanBackup.chan_points:type_name -> lnrpc.ChannelPoint + 206, // 165: lnrpc.ChanBackupSnapshot.single_chan_backups:type_name -> lnrpc.ChannelBackups + 203, // 166: lnrpc.ChanBackupSnapshot.multi_chan_backup:type_name -> lnrpc.MultiChanBackup + 202, // 167: lnrpc.ChannelBackups.chan_backups:type_name -> lnrpc.ChannelBackup + 206, // 168: lnrpc.RestoreChanBackupRequest.chan_backups:type_name -> lnrpc.ChannelBackups + 211, // 169: lnrpc.BakeMacaroonRequest.permissions:type_name -> lnrpc.MacaroonPermission + 211, // 170: lnrpc.MacaroonPermissionList.permissions:type_name -> lnrpc.MacaroonPermission + 260, // 171: lnrpc.ListPermissionsResponse.method_permissions:type_name -> lnrpc.ListPermissionsResponse.MethodPermissionsEntry + 20, // 172: lnrpc.Failure.code:type_name -> lnrpc.Failure.FailureCode + 222, // 173: lnrpc.Failure.channel_update:type_name -> lnrpc.ChannelUpdate + 224, // 174: lnrpc.MacaroonId.ops:type_name -> lnrpc.Op + 211, // 175: lnrpc.CheckMacPermRequest.permissions:type_name -> lnrpc.MacaroonPermission + 229, // 176: lnrpc.RPCMiddlewareRequest.stream_auth:type_name -> lnrpc.StreamAuth + 230, // 177: lnrpc.RPCMiddlewareRequest.request:type_name -> lnrpc.RPCMessage + 230, // 178: lnrpc.RPCMiddlewareRequest.response:type_name -> lnrpc.RPCMessage + 261, // 179: lnrpc.RPCMiddlewareRequest.metadata_pairs:type_name -> lnrpc.RPCMiddlewareRequest.MetadataPairsEntry + 232, // 180: lnrpc.RPCMiddlewareResponse.register:type_name -> lnrpc.MiddlewareRegistration + 233, // 181: lnrpc.RPCMiddlewareResponse.feedback:type_name -> lnrpc.InterceptFeedback + 190, // 182: lnrpc.Peer.FeaturesEntry.value:type_name -> lnrpc.Feature + 190, // 183: lnrpc.GetInfoResponse.FeaturesEntry.value:type_name -> lnrpc.Feature + 4, // 184: lnrpc.PendingChannelsResponse.PendingChannel.initiator:type_name -> lnrpc.Initiator + 3, // 185: lnrpc.PendingChannelsResponse.PendingChannel.commitment_type:type_name -> lnrpc.CommitmentType + 240, // 186: lnrpc.PendingChannelsResponse.PendingOpenChannel.channel:type_name -> lnrpc.PendingChannelsResponse.PendingChannel + 240, // 187: lnrpc.PendingChannelsResponse.WaitingCloseChannel.channel:type_name -> lnrpc.PendingChannelsResponse.PendingChannel + 243, // 188: lnrpc.PendingChannelsResponse.WaitingCloseChannel.commitments:type_name -> lnrpc.PendingChannelsResponse.Commitments + 240, // 189: lnrpc.PendingChannelsResponse.ClosedChannel.channel:type_name -> lnrpc.PendingChannelsResponse.PendingChannel + 240, // 190: lnrpc.PendingChannelsResponse.ForceClosedChannel.channel:type_name -> lnrpc.PendingChannelsResponse.PendingChannel + 112, // 191: lnrpc.PendingChannelsResponse.ForceClosedChannel.pending_htlcs:type_name -> lnrpc.PendingHTLC + 15, // 192: lnrpc.PendingChannelsResponse.ForceClosedChannel.anchor:type_name -> lnrpc.PendingChannelsResponse.ForceClosedChannel.AnchorState + 117, // 193: lnrpc.WalletBalanceResponse.AccountBalanceEntry.value:type_name -> lnrpc.WalletAccountBalance + 190, // 194: lnrpc.LightningNode.FeaturesEntry.value:type_name -> lnrpc.Feature + 142, // 195: lnrpc.NodeMetricsResponse.BetweennessCentralityEntry.value:type_name -> lnrpc.FloatMetric + 190, // 196: lnrpc.NodeUpdate.FeaturesEntry.value:type_name -> lnrpc.Feature + 190, // 197: lnrpc.Invoice.FeaturesEntry.value:type_name -> lnrpc.Feature + 159, // 198: lnrpc.Invoice.AmpInvoiceStateEntry.value:type_name -> lnrpc.AMPInvoiceState + 190, // 199: lnrpc.PayReq.FeaturesEntry.value:type_name -> lnrpc.Feature + 218, // 200: lnrpc.ListPermissionsResponse.MethodPermissionsEntry.value:type_name -> lnrpc.MacaroonPermissionList + 228, // 201: lnrpc.RPCMiddlewareRequest.MetadataPairsEntry.value:type_name -> lnrpc.MetadataValues + 118, // 202: lnrpc.Lightning.WalletBalance:input_type -> lnrpc.WalletBalanceRequest + 121, // 203: lnrpc.Lightning.ChannelBalance:input_type -> lnrpc.ChannelBalanceRequest + 34, // 204: lnrpc.Lightning.GetTransactions:input_type -> lnrpc.GetTransactionsRequest + 46, // 205: lnrpc.Lightning.EstimateFee:input_type -> lnrpc.EstimateFeeRequest + 50, // 206: lnrpc.Lightning.SendCoins:input_type -> lnrpc.SendCoinsRequest + 52, // 207: lnrpc.Lightning.ListUnspent:input_type -> lnrpc.ListUnspentRequest + 34, // 208: lnrpc.Lightning.SubscribeTransactions:input_type -> lnrpc.GetTransactionsRequest + 48, // 209: lnrpc.Lightning.SendMany:input_type -> lnrpc.SendManyRequest + 54, // 210: lnrpc.Lightning.NewAddress:input_type -> lnrpc.NewAddressRequest + 56, // 211: lnrpc.Lightning.SignMessage:input_type -> lnrpc.SignMessageRequest + 58, // 212: lnrpc.Lightning.VerifyMessage:input_type -> lnrpc.VerifyMessageRequest + 60, // 213: lnrpc.Lightning.ConnectPeer:input_type -> lnrpc.ConnectPeerRequest + 62, // 214: lnrpc.Lightning.DisconnectPeer:input_type -> lnrpc.DisconnectPeerRequest + 78, // 215: lnrpc.Lightning.ListPeers:input_type -> lnrpc.ListPeersRequest + 80, // 216: lnrpc.Lightning.SubscribePeerEvents:input_type -> lnrpc.PeerEventSubscription + 82, // 217: lnrpc.Lightning.GetInfo:input_type -> lnrpc.GetInfoRequest + 84, // 218: lnrpc.Lightning.GetDebugInfo:input_type -> lnrpc.GetDebugInfoRequest + 86, // 219: lnrpc.Lightning.GetRecoveryInfo:input_type -> lnrpc.GetRecoveryInfoRequest + 113, // 220: lnrpc.Lightning.PendingChannels:input_type -> lnrpc.PendingChannelsRequest + 67, // 221: lnrpc.Lightning.ListChannels:input_type -> lnrpc.ListChannelsRequest + 115, // 222: lnrpc.Lightning.SubscribeChannelEvents:input_type -> lnrpc.ChannelEventSubscription + 74, // 223: lnrpc.Lightning.ClosedChannels:input_type -> lnrpc.ClosedChannelsRequest + 100, // 224: lnrpc.Lightning.OpenChannelSync:input_type -> lnrpc.OpenChannelRequest + 100, // 225: lnrpc.Lightning.OpenChannel:input_type -> lnrpc.OpenChannelRequest + 97, // 226: lnrpc.Lightning.BatchOpenChannel:input_type -> lnrpc.BatchOpenChannelRequest + 110, // 227: lnrpc.Lightning.FundingStateStep:input_type -> lnrpc.FundingTransitionMsg + 41, // 228: lnrpc.Lightning.ChannelAcceptor:input_type -> lnrpc.ChannelAcceptResponse + 92, // 229: lnrpc.Lightning.CloseChannel:input_type -> lnrpc.CloseChannelRequest + 184, // 230: lnrpc.Lightning.AbandonChannel:input_type -> lnrpc.AbandonChannelRequest + 37, // 231: lnrpc.Lightning.SendPayment:input_type -> lnrpc.SendRequest + 37, // 232: lnrpc.Lightning.SendPaymentSync:input_type -> lnrpc.SendRequest + 39, // 233: lnrpc.Lightning.SendToRoute:input_type -> lnrpc.SendToRouteRequest + 39, // 234: lnrpc.Lightning.SendToRouteSync:input_type -> lnrpc.SendToRouteRequest + 160, // 235: lnrpc.Lightning.AddInvoice:input_type -> lnrpc.Invoice + 166, // 236: lnrpc.Lightning.ListInvoices:input_type -> lnrpc.ListInvoiceRequest + 165, // 237: lnrpc.Lightning.LookupInvoice:input_type -> lnrpc.PaymentHash + 168, // 238: lnrpc.Lightning.SubscribeInvoices:input_type -> lnrpc.InvoiceSubscription + 169, // 239: lnrpc.Lightning.DeleteCanceledInvoice:input_type -> lnrpc.DelCanceledInvoiceReq + 188, // 240: lnrpc.Lightning.DecodePayReq:input_type -> lnrpc.PayReqString + 173, // 241: lnrpc.Lightning.ListPayments:input_type -> lnrpc.ListPaymentsRequest + 175, // 242: lnrpc.Lightning.ListPaymentDuplicates:input_type -> lnrpc.ListPaymentDuplicatesRequest + 177, // 243: lnrpc.Lightning.ListAllPaymentDuplicates:input_type -> lnrpc.ListAllPaymentDuplicatesRequest + 180, // 244: lnrpc.Lightning.DeletePayment:input_type -> lnrpc.DeletePaymentRequest + 181, // 245: lnrpc.Lightning.DeleteAllPayments:input_type -> lnrpc.DeleteAllPaymentsRequest + 138, // 246: lnrpc.Lightning.DescribeGraph:input_type -> lnrpc.ChannelGraphRequest + 140, // 247: lnrpc.Lightning.GetNodeMetrics:input_type -> lnrpc.NodeMetricsRequest + 143, // 248: lnrpc.Lightning.GetChanInfo:input_type -> lnrpc.ChanInfoRequest + 131, // 249: lnrpc.Lightning.GetNodeInfo:input_type -> lnrpc.NodeInfoRequest + 123, // 250: lnrpc.Lightning.QueryRoutes:input_type -> lnrpc.QueryRoutesRequest + 144, // 251: lnrpc.Lightning.GetNetworkInfo:input_type -> lnrpc.NetworkInfoRequest + 146, // 252: lnrpc.Lightning.StopDaemon:input_type -> lnrpc.StopRequest + 148, // 253: lnrpc.Lightning.SubscribeChannelGraph:input_type -> lnrpc.GraphTopologySubscription + 186, // 254: lnrpc.Lightning.DebugLevel:input_type -> lnrpc.DebugLevelRequest + 191, // 255: lnrpc.Lightning.FeeReport:input_type -> lnrpc.FeeReportRequest + 195, // 256: lnrpc.Lightning.UpdateChannelPolicy:input_type -> lnrpc.PolicyUpdateRequest + 198, // 257: lnrpc.Lightning.ForwardingHistory:input_type -> lnrpc.ForwardingHistoryRequest + 201, // 258: lnrpc.Lightning.ExportChannelBackup:input_type -> lnrpc.ExportChannelBackupRequest + 204, // 259: lnrpc.Lightning.ExportAllChannelBackups:input_type -> lnrpc.ChanBackupExportRequest + 205, // 260: lnrpc.Lightning.VerifyChanBackup:input_type -> lnrpc.ChanBackupSnapshot + 207, // 261: lnrpc.Lightning.RestoreChannelBackups:input_type -> lnrpc.RestoreChanBackupRequest + 209, // 262: lnrpc.Lightning.SubscribeChannelBackups:input_type -> lnrpc.ChannelBackupSubscription + 212, // 263: lnrpc.Lightning.BakeMacaroon:input_type -> lnrpc.BakeMacaroonRequest + 214, // 264: lnrpc.Lightning.ListMacaroonIDs:input_type -> lnrpc.ListMacaroonIDsRequest + 216, // 265: lnrpc.Lightning.DeleteMacaroonID:input_type -> lnrpc.DeleteMacaroonIDRequest + 219, // 266: lnrpc.Lightning.ListPermissions:input_type -> lnrpc.ListPermissionsRequest + 225, // 267: lnrpc.Lightning.CheckMacaroonPermissions:input_type -> lnrpc.CheckMacPermRequest + 231, // 268: lnrpc.Lightning.RegisterRPCMiddleware:input_type -> lnrpc.RPCMiddlewareResponse + 25, // 269: lnrpc.Lightning.SendCustomMessage:input_type -> lnrpc.SendCustomMessageRequest + 23, // 270: lnrpc.Lightning.SubscribeCustomMessages:input_type -> lnrpc.SubscribeCustomMessagesRequest + 29, // 271: lnrpc.Lightning.SendOnionMessage:input_type -> lnrpc.SendOnionMessageRequest + 27, // 272: lnrpc.Lightning.SubscribeOnionMessages:input_type -> lnrpc.SubscribeOnionMessagesRequest + 70, // 273: lnrpc.Lightning.ListAliases:input_type -> lnrpc.ListAliasesRequest + 21, // 274: lnrpc.Lightning.LookupHtlcResolution:input_type -> lnrpc.LookupHtlcResolutionRequest + 119, // 275: lnrpc.Lightning.WalletBalance:output_type -> lnrpc.WalletBalanceResponse + 122, // 276: lnrpc.Lightning.ChannelBalance:output_type -> lnrpc.ChannelBalanceResponse + 35, // 277: lnrpc.Lightning.GetTransactions:output_type -> lnrpc.TransactionDetails + 47, // 278: lnrpc.Lightning.EstimateFee:output_type -> lnrpc.EstimateFeeResponse + 51, // 279: lnrpc.Lightning.SendCoins:output_type -> lnrpc.SendCoinsResponse + 53, // 280: lnrpc.Lightning.ListUnspent:output_type -> lnrpc.ListUnspentResponse + 33, // 281: lnrpc.Lightning.SubscribeTransactions:output_type -> lnrpc.Transaction + 49, // 282: lnrpc.Lightning.SendMany:output_type -> lnrpc.SendManyResponse + 55, // 283: lnrpc.Lightning.NewAddress:output_type -> lnrpc.NewAddressResponse + 57, // 284: lnrpc.Lightning.SignMessage:output_type -> lnrpc.SignMessageResponse + 59, // 285: lnrpc.Lightning.VerifyMessage:output_type -> lnrpc.VerifyMessageResponse + 61, // 286: lnrpc.Lightning.ConnectPeer:output_type -> lnrpc.ConnectPeerResponse + 63, // 287: lnrpc.Lightning.DisconnectPeer:output_type -> lnrpc.DisconnectPeerResponse + 79, // 288: lnrpc.Lightning.ListPeers:output_type -> lnrpc.ListPeersResponse + 81, // 289: lnrpc.Lightning.SubscribePeerEvents:output_type -> lnrpc.PeerEvent + 83, // 290: lnrpc.Lightning.GetInfo:output_type -> lnrpc.GetInfoResponse + 85, // 291: lnrpc.Lightning.GetDebugInfo:output_type -> lnrpc.GetDebugInfoResponse + 87, // 292: lnrpc.Lightning.GetRecoveryInfo:output_type -> lnrpc.GetRecoveryInfoResponse + 114, // 293: lnrpc.Lightning.PendingChannels:output_type -> lnrpc.PendingChannelsResponse + 68, // 294: lnrpc.Lightning.ListChannels:output_type -> lnrpc.ListChannelsResponse + 116, // 295: lnrpc.Lightning.SubscribeChannelEvents:output_type -> lnrpc.ChannelEventUpdate + 75, // 296: lnrpc.Lightning.ClosedChannels:output_type -> lnrpc.ClosedChannelsResponse + 42, // 297: lnrpc.Lightning.OpenChannelSync:output_type -> lnrpc.ChannelPoint + 101, // 298: lnrpc.Lightning.OpenChannel:output_type -> lnrpc.OpenStatusUpdate + 99, // 299: lnrpc.Lightning.BatchOpenChannel:output_type -> lnrpc.BatchOpenChannelResponse + 111, // 300: lnrpc.Lightning.FundingStateStep:output_type -> lnrpc.FundingStateStepResp + 40, // 301: lnrpc.Lightning.ChannelAcceptor:output_type -> lnrpc.ChannelAcceptRequest + 93, // 302: lnrpc.Lightning.CloseChannel:output_type -> lnrpc.CloseStatusUpdate + 185, // 303: lnrpc.Lightning.AbandonChannel:output_type -> lnrpc.AbandonChannelResponse + 38, // 304: lnrpc.Lightning.SendPayment:output_type -> lnrpc.SendResponse + 38, // 305: lnrpc.Lightning.SendPaymentSync:output_type -> lnrpc.SendResponse + 38, // 306: lnrpc.Lightning.SendToRoute:output_type -> lnrpc.SendResponse + 38, // 307: lnrpc.Lightning.SendToRouteSync:output_type -> lnrpc.SendResponse + 164, // 308: lnrpc.Lightning.AddInvoice:output_type -> lnrpc.AddInvoiceResponse + 167, // 309: lnrpc.Lightning.ListInvoices:output_type -> lnrpc.ListInvoiceResponse + 160, // 310: lnrpc.Lightning.LookupInvoice:output_type -> lnrpc.Invoice + 160, // 311: lnrpc.Lightning.SubscribeInvoices:output_type -> lnrpc.Invoice + 170, // 312: lnrpc.Lightning.DeleteCanceledInvoice:output_type -> lnrpc.DelCanceledInvoiceResp + 189, // 313: lnrpc.Lightning.DecodePayReq:output_type -> lnrpc.PayReq + 174, // 314: lnrpc.Lightning.ListPayments:output_type -> lnrpc.ListPaymentsResponse + 176, // 315: lnrpc.Lightning.ListPaymentDuplicates:output_type -> lnrpc.ListPaymentDuplicatesResponse + 178, // 316: lnrpc.Lightning.ListAllPaymentDuplicates:output_type -> lnrpc.ListAllPaymentDuplicatesResponse + 182, // 317: lnrpc.Lightning.DeletePayment:output_type -> lnrpc.DeletePaymentResponse + 183, // 318: lnrpc.Lightning.DeleteAllPayments:output_type -> lnrpc.DeleteAllPaymentsResponse + 139, // 319: lnrpc.Lightning.DescribeGraph:output_type -> lnrpc.ChannelGraph + 141, // 320: lnrpc.Lightning.GetNodeMetrics:output_type -> lnrpc.NodeMetricsResponse + 137, // 321: lnrpc.Lightning.GetChanInfo:output_type -> lnrpc.ChannelEdge + 132, // 322: lnrpc.Lightning.GetNodeInfo:output_type -> lnrpc.NodeInfo + 126, // 323: lnrpc.Lightning.QueryRoutes:output_type -> lnrpc.QueryRoutesResponse + 145, // 324: lnrpc.Lightning.GetNetworkInfo:output_type -> lnrpc.NetworkInfo + 147, // 325: lnrpc.Lightning.StopDaemon:output_type -> lnrpc.StopResponse + 149, // 326: lnrpc.Lightning.SubscribeChannelGraph:output_type -> lnrpc.GraphTopologyUpdate + 187, // 327: lnrpc.Lightning.DebugLevel:output_type -> lnrpc.DebugLevelResponse + 193, // 328: lnrpc.Lightning.FeeReport:output_type -> lnrpc.FeeReportResponse + 197, // 329: lnrpc.Lightning.UpdateChannelPolicy:output_type -> lnrpc.PolicyUpdateResponse + 200, // 330: lnrpc.Lightning.ForwardingHistory:output_type -> lnrpc.ForwardingHistoryResponse + 202, // 331: lnrpc.Lightning.ExportChannelBackup:output_type -> lnrpc.ChannelBackup + 205, // 332: lnrpc.Lightning.ExportAllChannelBackups:output_type -> lnrpc.ChanBackupSnapshot + 210, // 333: lnrpc.Lightning.VerifyChanBackup:output_type -> lnrpc.VerifyChanBackupResponse + 208, // 334: lnrpc.Lightning.RestoreChannelBackups:output_type -> lnrpc.RestoreBackupResponse + 205, // 335: lnrpc.Lightning.SubscribeChannelBackups:output_type -> lnrpc.ChanBackupSnapshot + 213, // 336: lnrpc.Lightning.BakeMacaroon:output_type -> lnrpc.BakeMacaroonResponse + 215, // 337: lnrpc.Lightning.ListMacaroonIDs:output_type -> lnrpc.ListMacaroonIDsResponse + 217, // 338: lnrpc.Lightning.DeleteMacaroonID:output_type -> lnrpc.DeleteMacaroonIDResponse + 220, // 339: lnrpc.Lightning.ListPermissions:output_type -> lnrpc.ListPermissionsResponse + 226, // 340: lnrpc.Lightning.CheckMacaroonPermissions:output_type -> lnrpc.CheckMacPermResponse + 227, // 341: lnrpc.Lightning.RegisterRPCMiddleware:output_type -> lnrpc.RPCMiddlewareRequest + 26, // 342: lnrpc.Lightning.SendCustomMessage:output_type -> lnrpc.SendCustomMessageResponse + 24, // 343: lnrpc.Lightning.SubscribeCustomMessages:output_type -> lnrpc.CustomMessage + 30, // 344: lnrpc.Lightning.SendOnionMessage:output_type -> lnrpc.SendOnionMessageResponse + 28, // 345: lnrpc.Lightning.SubscribeOnionMessages:output_type -> lnrpc.OnionMessage + 71, // 346: lnrpc.Lightning.ListAliases:output_type -> lnrpc.ListAliasesResponse + 22, // 347: lnrpc.Lightning.LookupHtlcResolution:output_type -> lnrpc.LookupHtlcResolutionResponse + 275, // [275:348] is the sub-list for method output_type + 202, // [202:275] is the sub-list for method input_type + 202, // [202:202] is the sub-list for extension type_name + 202, // [202:202] is the sub-list for extension extendee + 0, // [0:202] is the sub-list for field type_name } func init() { file_lightning_proto_init() } @@ -24949,7 +25284,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[154].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeletePaymentRequest); i { + switch v := v.(*ListPaymentDuplicatesRequest); i { case 0: return &v.state case 1: @@ -24961,7 +25296,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[155].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeleteAllPaymentsRequest); i { + switch v := v.(*ListPaymentDuplicatesResponse); i { case 0: return &v.state case 1: @@ -24973,7 +25308,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[156].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeletePaymentResponse); i { + switch v := v.(*ListAllPaymentDuplicatesRequest); i { case 0: return &v.state case 1: @@ -24985,7 +25320,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[157].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeleteAllPaymentsResponse); i { + switch v := v.(*ListAllPaymentDuplicatesResponse); i { case 0: return &v.state case 1: @@ -24997,7 +25332,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[158].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AbandonChannelRequest); i { + switch v := v.(*PaymentDuplicate); i { case 0: return &v.state case 1: @@ -25009,7 +25344,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[159].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AbandonChannelResponse); i { + switch v := v.(*DeletePaymentRequest); i { case 0: return &v.state case 1: @@ -25021,7 +25356,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[160].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DebugLevelRequest); i { + switch v := v.(*DeleteAllPaymentsRequest); i { case 0: return &v.state case 1: @@ -25033,7 +25368,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[161].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DebugLevelResponse); i { + switch v := v.(*DeletePaymentResponse); i { case 0: return &v.state case 1: @@ -25045,7 +25380,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[162].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PayReqString); i { + switch v := v.(*DeleteAllPaymentsResponse); i { case 0: return &v.state case 1: @@ -25057,7 +25392,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[163].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PayReq); i { + switch v := v.(*AbandonChannelRequest); i { case 0: return &v.state case 1: @@ -25069,7 +25404,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[164].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Feature); i { + switch v := v.(*AbandonChannelResponse); i { case 0: return &v.state case 1: @@ -25081,7 +25416,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[165].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*FeeReportRequest); i { + switch v := v.(*DebugLevelRequest); i { case 0: return &v.state case 1: @@ -25093,7 +25428,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[166].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ChannelFeeReport); i { + switch v := v.(*DebugLevelResponse); i { case 0: return &v.state case 1: @@ -25105,7 +25440,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[167].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*FeeReportResponse); i { + switch v := v.(*PayReqString); i { case 0: return &v.state case 1: @@ -25117,7 +25452,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[168].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*InboundFee); i { + switch v := v.(*PayReq); i { case 0: return &v.state case 1: @@ -25129,7 +25464,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[169].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PolicyUpdateRequest); i { + switch v := v.(*Feature); i { case 0: return &v.state case 1: @@ -25141,7 +25476,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[170].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*FailedUpdate); i { + switch v := v.(*FeeReportRequest); i { case 0: return &v.state case 1: @@ -25153,7 +25488,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[171].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PolicyUpdateResponse); i { + switch v := v.(*ChannelFeeReport); i { case 0: return &v.state case 1: @@ -25165,7 +25500,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[172].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ForwardingHistoryRequest); i { + switch v := v.(*FeeReportResponse); i { case 0: return &v.state case 1: @@ -25177,7 +25512,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[173].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ForwardingEvent); i { + switch v := v.(*InboundFee); i { case 0: return &v.state case 1: @@ -25189,7 +25524,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[174].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ForwardingHistoryResponse); i { + switch v := v.(*PolicyUpdateRequest); i { case 0: return &v.state case 1: @@ -25201,7 +25536,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[175].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ExportChannelBackupRequest); i { + switch v := v.(*FailedUpdate); i { case 0: return &v.state case 1: @@ -25213,7 +25548,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[176].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ChannelBackup); i { + switch v := v.(*PolicyUpdateResponse); i { case 0: return &v.state case 1: @@ -25225,7 +25560,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[177].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*MultiChanBackup); i { + switch v := v.(*ForwardingHistoryRequest); i { case 0: return &v.state case 1: @@ -25237,7 +25572,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[178].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ChanBackupExportRequest); i { + switch v := v.(*ForwardingEvent); i { case 0: return &v.state case 1: @@ -25249,7 +25584,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[179].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ChanBackupSnapshot); i { + switch v := v.(*ForwardingHistoryResponse); i { case 0: return &v.state case 1: @@ -25261,7 +25596,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[180].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ChannelBackups); i { + switch v := v.(*ExportChannelBackupRequest); i { case 0: return &v.state case 1: @@ -25273,7 +25608,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[181].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RestoreChanBackupRequest); i { + switch v := v.(*ChannelBackup); i { case 0: return &v.state case 1: @@ -25285,7 +25620,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[182].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RestoreBackupResponse); i { + switch v := v.(*MultiChanBackup); i { case 0: return &v.state case 1: @@ -25297,7 +25632,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[183].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ChannelBackupSubscription); i { + switch v := v.(*ChanBackupExportRequest); i { case 0: return &v.state case 1: @@ -25309,7 +25644,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[184].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*VerifyChanBackupResponse); i { + switch v := v.(*ChanBackupSnapshot); i { case 0: return &v.state case 1: @@ -25321,7 +25656,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[185].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*MacaroonPermission); i { + switch v := v.(*ChannelBackups); i { case 0: return &v.state case 1: @@ -25333,7 +25668,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[186].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*BakeMacaroonRequest); i { + switch v := v.(*RestoreChanBackupRequest); i { case 0: return &v.state case 1: @@ -25345,7 +25680,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[187].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*BakeMacaroonResponse); i { + switch v := v.(*RestoreBackupResponse); i { case 0: return &v.state case 1: @@ -25357,7 +25692,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[188].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListMacaroonIDsRequest); i { + switch v := v.(*ChannelBackupSubscription); i { case 0: return &v.state case 1: @@ -25369,7 +25704,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[189].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListMacaroonIDsResponse); i { + switch v := v.(*VerifyChanBackupResponse); i { case 0: return &v.state case 1: @@ -25381,7 +25716,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[190].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeleteMacaroonIDRequest); i { + switch v := v.(*MacaroonPermission); i { case 0: return &v.state case 1: @@ -25393,7 +25728,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[191].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeleteMacaroonIDResponse); i { + switch v := v.(*BakeMacaroonRequest); i { case 0: return &v.state case 1: @@ -25405,7 +25740,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[192].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*MacaroonPermissionList); i { + switch v := v.(*BakeMacaroonResponse); i { case 0: return &v.state case 1: @@ -25417,7 +25752,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[193].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListPermissionsRequest); i { + switch v := v.(*ListMacaroonIDsRequest); i { case 0: return &v.state case 1: @@ -25429,7 +25764,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[194].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListPermissionsResponse); i { + switch v := v.(*ListMacaroonIDsResponse); i { case 0: return &v.state case 1: @@ -25441,7 +25776,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[195].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Failure); i { + switch v := v.(*DeleteMacaroonIDRequest); i { case 0: return &v.state case 1: @@ -25453,7 +25788,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[196].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ChannelUpdate); i { + switch v := v.(*DeleteMacaroonIDResponse); i { case 0: return &v.state case 1: @@ -25465,7 +25800,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[197].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*MacaroonId); i { + switch v := v.(*MacaroonPermissionList); i { case 0: return &v.state case 1: @@ -25477,7 +25812,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[198].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Op); i { + switch v := v.(*ListPermissionsRequest); i { case 0: return &v.state case 1: @@ -25489,7 +25824,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[199].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CheckMacPermRequest); i { + switch v := v.(*ListPermissionsResponse); i { case 0: return &v.state case 1: @@ -25501,7 +25836,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[200].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CheckMacPermResponse); i { + switch v := v.(*Failure); i { case 0: return &v.state case 1: @@ -25513,7 +25848,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[201].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RPCMiddlewareRequest); i { + switch v := v.(*ChannelUpdate); i { case 0: return &v.state case 1: @@ -25525,7 +25860,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[202].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*MetadataValues); i { + switch v := v.(*MacaroonId); i { case 0: return &v.state case 1: @@ -25537,7 +25872,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[203].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*StreamAuth); i { + switch v := v.(*Op); i { case 0: return &v.state case 1: @@ -25549,7 +25884,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[204].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RPCMessage); i { + switch v := v.(*CheckMacPermRequest); i { case 0: return &v.state case 1: @@ -25561,7 +25896,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[205].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RPCMiddlewareResponse); i { + switch v := v.(*CheckMacPermResponse); i { case 0: return &v.state case 1: @@ -25573,7 +25908,7 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[206].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*MiddlewareRegistration); i { + switch v := v.(*RPCMiddlewareRequest); i { case 0: return &v.state case 1: @@ -25585,6 +25920,66 @@ func file_lightning_proto_init() { } } file_lightning_proto_msgTypes[207].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*MetadataValues); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_lightning_proto_msgTypes[208].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*StreamAuth); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_lightning_proto_msgTypes[209].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RPCMessage); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_lightning_proto_msgTypes[210].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RPCMiddlewareResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_lightning_proto_msgTypes[211].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*MiddlewareRegistration); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_lightning_proto_msgTypes[212].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*InterceptFeedback); i { case 0: return &v.state @@ -25596,7 +25991,7 @@ func file_lightning_proto_init() { return nil } } - file_lightning_proto_msgTypes[214].Exporter = func(v interface{}, i int) interface{} { + file_lightning_proto_msgTypes[219].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*PendingChannelsResponse_PendingChannel); i { case 0: return &v.state @@ -25608,7 +26003,7 @@ func file_lightning_proto_init() { return nil } } - file_lightning_proto_msgTypes[215].Exporter = func(v interface{}, i int) interface{} { + file_lightning_proto_msgTypes[220].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*PendingChannelsResponse_PendingOpenChannel); i { case 0: return &v.state @@ -25620,7 +26015,7 @@ func file_lightning_proto_init() { return nil } } - file_lightning_proto_msgTypes[216].Exporter = func(v interface{}, i int) interface{} { + file_lightning_proto_msgTypes[221].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*PendingChannelsResponse_WaitingCloseChannel); i { case 0: return &v.state @@ -25632,7 +26027,7 @@ func file_lightning_proto_init() { return nil } } - file_lightning_proto_msgTypes[217].Exporter = func(v interface{}, i int) interface{} { + file_lightning_proto_msgTypes[222].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*PendingChannelsResponse_Commitments); i { case 0: return &v.state @@ -25644,7 +26039,7 @@ func file_lightning_proto_init() { return nil } } - file_lightning_proto_msgTypes[218].Exporter = func(v interface{}, i int) interface{} { + file_lightning_proto_msgTypes[223].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*PendingChannelsResponse_ClosedChannel); i { case 0: return &v.state @@ -25656,7 +26051,7 @@ func file_lightning_proto_init() { return nil } } - file_lightning_proto_msgTypes[219].Exporter = func(v interface{}, i int) interface{} { + file_lightning_proto_msgTypes[224].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*PendingChannelsResponse_ForceClosedChannel); i { case 0: return &v.state @@ -25708,22 +26103,22 @@ func file_lightning_proto_init() { (*ChannelEventUpdate_ChannelFundingTimeout)(nil), } file_lightning_proto_msgTypes[140].OneofWrappers = []interface{}{} - file_lightning_proto_msgTypes[169].OneofWrappers = []interface{}{ + file_lightning_proto_msgTypes[174].OneofWrappers = []interface{}{ (*PolicyUpdateRequest_Global)(nil), (*PolicyUpdateRequest_ChanPoint)(nil), } - file_lightning_proto_msgTypes[173].OneofWrappers = []interface{}{} - file_lightning_proto_msgTypes[181].OneofWrappers = []interface{}{ + file_lightning_proto_msgTypes[178].OneofWrappers = []interface{}{} + file_lightning_proto_msgTypes[186].OneofWrappers = []interface{}{ (*RestoreChanBackupRequest_ChanBackups)(nil), (*RestoreChanBackupRequest_MultiChanBackup)(nil), } - file_lightning_proto_msgTypes[201].OneofWrappers = []interface{}{ + file_lightning_proto_msgTypes[206].OneofWrappers = []interface{}{ (*RPCMiddlewareRequest_StreamAuth)(nil), (*RPCMiddlewareRequest_Request)(nil), (*RPCMiddlewareRequest_Response)(nil), (*RPCMiddlewareRequest_RegComplete)(nil), } - file_lightning_proto_msgTypes[205].OneofWrappers = []interface{}{ + file_lightning_proto_msgTypes[210].OneofWrappers = []interface{}{ (*RPCMiddlewareResponse_Register)(nil), (*RPCMiddlewareResponse_Feedback)(nil), } @@ -25733,7 +26128,7 @@ func file_lightning_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_lightning_proto_rawDesc, NumEnums: 21, - NumMessages: 236, + NumMessages: 241, NumExtensions: 0, NumServices: 1, }, diff --git a/lnrpc/lightning.pb.gw.go b/lnrpc/lightning.pb.gw.go index bcebf578e33..f3ea64818d9 100644 --- a/lnrpc/lightning.pb.gw.go +++ b/lnrpc/lightning.pb.gw.go @@ -1429,6 +1429,58 @@ func local_request_Lightning_ListPayments_0(ctx context.Context, marshaler runti } +func request_Lightning_ListPaymentDuplicates_0(ctx context.Context, marshaler runtime.Marshaler, client LightningClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ListPaymentDuplicatesRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.ListPaymentDuplicates(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_Lightning_ListPaymentDuplicates_0(ctx context.Context, marshaler runtime.Marshaler, server LightningServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ListPaymentDuplicatesRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.ListPaymentDuplicates(ctx, &protoReq) + return msg, metadata, err + +} + +func request_Lightning_ListAllPaymentDuplicates_0(ctx context.Context, marshaler runtime.Marshaler, client LightningClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ListAllPaymentDuplicatesRequest + var metadata runtime.ServerMetadata + + msg, err := client.ListAllPaymentDuplicates(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_Lightning_ListAllPaymentDuplicates_0(ctx context.Context, marshaler runtime.Marshaler, server LightningServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ListAllPaymentDuplicatesRequest + var metadata runtime.ServerMetadata + + msg, err := server.ListAllPaymentDuplicates(ctx, &protoReq) + return msg, metadata, err + +} + var ( filter_Lightning_DeletePayment_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} ) @@ -3501,6 +3553,56 @@ func RegisterLightningHandlerServer(ctx context.Context, mux *runtime.ServeMux, }) + mux.Handle("POST", pattern_Lightning_ListPaymentDuplicates_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/lnrpc.Lightning/ListPaymentDuplicates", runtime.WithHTTPPathPattern("/v1/payments/duplicates")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_Lightning_ListPaymentDuplicates_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_Lightning_ListPaymentDuplicates_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_Lightning_ListAllPaymentDuplicates_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/lnrpc.Lightning/ListAllPaymentDuplicates", runtime.WithHTTPPathPattern("/v1/payments/duplicates")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_Lightning_ListAllPaymentDuplicates_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_Lightning_ListAllPaymentDuplicates_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("DELETE", pattern_Lightning_DeletePayment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -5110,6 +5212,50 @@ func RegisterLightningHandlerClient(ctx context.Context, mux *runtime.ServeMux, }) + mux.Handle("POST", pattern_Lightning_ListPaymentDuplicates_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/lnrpc.Lightning/ListPaymentDuplicates", runtime.WithHTTPPathPattern("/v1/payments/duplicates")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_Lightning_ListPaymentDuplicates_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_Lightning_ListPaymentDuplicates_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_Lightning_ListAllPaymentDuplicates_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/lnrpc.Lightning/ListAllPaymentDuplicates", runtime.WithHTTPPathPattern("/v1/payments/duplicates")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_Lightning_ListAllPaymentDuplicates_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_Lightning_ListAllPaymentDuplicates_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("DELETE", pattern_Lightning_DeletePayment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -5896,6 +6042,10 @@ var ( pattern_Lightning_ListPayments_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "payments"}, "")) + pattern_Lightning_ListPaymentDuplicates_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "payments", "duplicates"}, "")) + + pattern_Lightning_ListAllPaymentDuplicates_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "payments", "duplicates"}, "")) + pattern_Lightning_DeletePayment_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "payment"}, "")) pattern_Lightning_DeleteAllPayments_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "payments"}, "")) @@ -6040,6 +6190,10 @@ var ( forward_Lightning_ListPayments_0 = runtime.ForwardResponseMessage + forward_Lightning_ListPaymentDuplicates_0 = runtime.ForwardResponseMessage + + forward_Lightning_ListAllPaymentDuplicates_0 = runtime.ForwardResponseMessage + forward_Lightning_DeletePayment_0 = runtime.ForwardResponseMessage forward_Lightning_DeleteAllPayments_0 = runtime.ForwardResponseMessage diff --git a/lnrpc/lightning.pb.json.go b/lnrpc/lightning.pb.json.go index 1fee502fab3..02b46ac2141 100644 --- a/lnrpc/lightning.pb.json.go +++ b/lnrpc/lightning.pb.json.go @@ -1048,6 +1048,56 @@ func RegisterLightningJSONCallbacks(registry map[string]func(ctx context.Context callback(string(respBytes), nil) } + registry["lnrpc.Lightning.ListPaymentDuplicates"] = func(ctx context.Context, + conn *grpc.ClientConn, reqJSON string, callback func(string, error)) { + + req := &ListPaymentDuplicatesRequest{} + err := marshaler.Unmarshal([]byte(reqJSON), req) + if err != nil { + callback("", err) + return + } + + client := NewLightningClient(conn) + resp, err := client.ListPaymentDuplicates(ctx, req) + if err != nil { + callback("", err) + return + } + + respBytes, err := marshaler.Marshal(resp) + if err != nil { + callback("", err) + return + } + callback(string(respBytes), nil) + } + + registry["lnrpc.Lightning.ListAllPaymentDuplicates"] = func(ctx context.Context, + conn *grpc.ClientConn, reqJSON string, callback func(string, error)) { + + req := &ListAllPaymentDuplicatesRequest{} + err := marshaler.Unmarshal([]byte(reqJSON), req) + if err != nil { + callback("", err) + return + } + + client := NewLightningClient(conn) + resp, err := client.ListAllPaymentDuplicates(ctx, req) + if err != nil { + callback("", err) + return + } + + respBytes, err := marshaler.Marshal(resp) + if err != nil { + callback("", err) + return + } + callback(string(respBytes), nil) + } + registry["lnrpc.Lightning.DeletePayment"] = func(ctx context.Context, conn *grpc.ClientConn, reqJSON string, callback func(string, error)) { diff --git a/lnrpc/lightning.proto b/lnrpc/lightning.proto index 9625934f7e8..1f4232bce94 100644 --- a/lnrpc/lightning.proto +++ b/lnrpc/lightning.proto @@ -360,6 +360,20 @@ service Lightning { */ rpc ListPayments (ListPaymentsRequest) returns (ListPaymentsResponse); + /* lncli: `listpaymentduplicates` + ListPaymentDuplicates returns duplicate payment records for the given + payment hash. This RPC is only supported by the SQL payments backend. + */ + rpc ListPaymentDuplicates (ListPaymentDuplicatesRequest) + returns (ListPaymentDuplicatesResponse); + + /* lncli: `listallpaymentduplicates` + ListAllPaymentDuplicates returns duplicate payment records across all + payments. This RPC is only supported by the SQL payments backend. + */ + rpc ListAllPaymentDuplicates (ListAllPaymentDuplicatesRequest) + returns (ListAllPaymentDuplicatesResponse); + /* lncli: `deletepayments` DeletePayment deletes an outgoing payment from DB. Note that it will not attempt to delete an In-Flight payment, since that would be unsafe. @@ -4554,6 +4568,44 @@ message ListPaymentsResponse { uint64 total_num_payments = 4; } +message ListPaymentDuplicatesRequest { + // The payment hash whose duplicates should be returned. + bytes payment_hash = 1; +} + +message ListPaymentDuplicatesResponse { + // The list of duplicate payment records for the given payment. + repeated PaymentDuplicate duplicates = 1; +} + +message ListAllPaymentDuplicatesRequest { +} + +message ListAllPaymentDuplicatesResponse { + // The list of duplicate payment records across all payments. + repeated PaymentDuplicate duplicates = 1; +} + +message PaymentDuplicate { + // The payment hash for the duplicate payment. + bytes payment_hash = 1; + + // The value of the duplicate payment in milli-satoshis. + int64 value_msat = 2; + + // The time in UNIX nanoseconds at which the duplicate was created. + int64 creation_time_ns = 3; + + // The failure reason for failed duplicates. + PaymentFailureReason failure_reason = 4; + + // The payment preimage for settled duplicates. + bytes payment_preimage = 5; + + // The time in UNIX nanoseconds at which the duplicate was settled. + int64 settle_time_ns = 6; +} + message DeletePaymentRequest { // Payment hash to delete. bytes payment_hash = 1; diff --git a/lnrpc/lightning.swagger.json b/lnrpc/lightning.swagger.json index 5f796a5d7c6..286f3b58ce4 100644 --- a/lnrpc/lightning.swagger.json +++ b/lnrpc/lightning.swagger.json @@ -2491,6 +2491,60 @@ ] } }, + "/v1/payments/duplicates": { + "get": { + "summary": "lncli: `listallpaymentduplicates`\nListAllPaymentDuplicates returns duplicate payment records across all\npayments. This RPC is only supported by the SQL payments backend.", + "operationId": "Lightning_ListAllPaymentDuplicates", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/lnrpcListAllPaymentDuplicatesResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "tags": [ + "Lightning" + ] + }, + "post": { + "summary": "lncli: `listpaymentduplicates`\nListPaymentDuplicates returns duplicate payment records for the given\npayment hash. This RPC is only supported by the SQL payments backend.", + "operationId": "Lightning_ListPaymentDuplicates", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/lnrpcListPaymentDuplicatesResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/lnrpcListPaymentDuplicatesRequest" + } + } + ], + "tags": [ + "Lightning" + ] + } + }, "/v1/payreq/{pay_req}": { "get": { "summary": "lncli: `decodepayreq`\nDecodePayReq takes an encoded payment request string and attempts to decode\nit, returning a full description of the conditions encoded within the\npayment request.", @@ -6162,6 +6216,19 @@ } } }, + "lnrpcListAllPaymentDuplicatesResponse": { + "type": "object", + "properties": { + "duplicates": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/lnrpcPaymentDuplicate" + }, + "description": "The list of duplicate payment records across all payments." + } + } + }, "lnrpcListChannelsResponse": { "type": "object", "properties": { @@ -6211,6 +6278,29 @@ } } }, + "lnrpcListPaymentDuplicatesRequest": { + "type": "object", + "properties": { + "payment_hash": { + "type": "string", + "format": "byte", + "description": "The payment hash whose duplicates should be returned." + } + } + }, + "lnrpcListPaymentDuplicatesResponse": { + "type": "object", + "properties": { + "duplicates": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/lnrpcPaymentDuplicate" + }, + "description": "The list of duplicate payment records for the given payment." + } + } + }, "lnrpcListPaymentsResponse": { "type": "object", "properties": { @@ -6943,6 +7033,40 @@ } } }, + "lnrpcPaymentDuplicate": { + "type": "object", + "properties": { + "payment_hash": { + "type": "string", + "format": "byte", + "description": "The payment hash for the duplicate payment." + }, + "value_msat": { + "type": "string", + "format": "int64", + "description": "The value of the duplicate payment in milli-satoshis." + }, + "creation_time_ns": { + "type": "string", + "format": "int64", + "description": "The time in UNIX nanoseconds at which the duplicate was created." + }, + "failure_reason": { + "$ref": "#/definitions/lnrpcPaymentFailureReason", + "description": "The failure reason for failed duplicates." + }, + "payment_preimage": { + "type": "string", + "format": "byte", + "description": "The payment preimage for settled duplicates." + }, + "settle_time_ns": { + "type": "string", + "format": "int64", + "description": "The time in UNIX nanoseconds at which the duplicate was settled." + } + } + }, "lnrpcPaymentFailureReason": { "type": "string", "enum": [ diff --git a/lnrpc/lightning.yaml b/lnrpc/lightning.yaml index 457f0fe06d9..81e97943d24 100644 --- a/lnrpc/lightning.yaml +++ b/lnrpc/lightning.yaml @@ -103,6 +103,11 @@ http: delete: "/v1/payment" - selector: lnrpc.Lightning.ListPayments get: "/v1/payments" + - selector: lnrpc.Lightning.ListPaymentDuplicates + post: "/v1/payments/duplicates" + body: "*" + - selector: lnrpc.Lightning.ListAllPaymentDuplicates + get: "/v1/payments/duplicates" - selector: lnrpc.Lightning.DeleteAllPayments delete: "/v1/payments" - selector: lnrpc.Lightning.DescribeGraph diff --git a/lnrpc/lightning_grpc.pb.go b/lnrpc/lightning_grpc.pb.go index 19e0165fd78..893f082be1d 100644 --- a/lnrpc/lightning_grpc.pb.go +++ b/lnrpc/lightning_grpc.pb.go @@ -255,6 +255,14 @@ type LightningClient interface { // lncli: `listpayments` // ListPayments returns a list of all outgoing payments. ListPayments(ctx context.Context, in *ListPaymentsRequest, opts ...grpc.CallOption) (*ListPaymentsResponse, error) + // lncli: `listpaymentduplicates` + // ListPaymentDuplicates returns duplicate payment records for the given + // payment hash. This RPC is only supported by the SQL payments backend. + ListPaymentDuplicates(ctx context.Context, in *ListPaymentDuplicatesRequest, opts ...grpc.CallOption) (*ListPaymentDuplicatesResponse, error) + // lncli: `listallpaymentduplicates` + // ListAllPaymentDuplicates returns duplicate payment records across all + // payments. This RPC is only supported by the SQL payments backend. + ListAllPaymentDuplicates(ctx context.Context, in *ListAllPaymentDuplicatesRequest, opts ...grpc.CallOption) (*ListAllPaymentDuplicatesResponse, error) // lncli: `deletepayments` // DeletePayment deletes an outgoing payment from DB. Note that it will not // attempt to delete an In-Flight payment, since that would be unsafe. @@ -1010,6 +1018,24 @@ func (c *lightningClient) ListPayments(ctx context.Context, in *ListPaymentsRequ return out, nil } +func (c *lightningClient) ListPaymentDuplicates(ctx context.Context, in *ListPaymentDuplicatesRequest, opts ...grpc.CallOption) (*ListPaymentDuplicatesResponse, error) { + out := new(ListPaymentDuplicatesResponse) + err := c.cc.Invoke(ctx, "/lnrpc.Lightning/ListPaymentDuplicates", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *lightningClient) ListAllPaymentDuplicates(ctx context.Context, in *ListAllPaymentDuplicatesRequest, opts ...grpc.CallOption) (*ListAllPaymentDuplicatesResponse, error) { + out := new(ListAllPaymentDuplicatesResponse) + err := c.cc.Invoke(ctx, "/lnrpc.Lightning/ListAllPaymentDuplicates", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *lightningClient) DeletePayment(ctx context.Context, in *DeletePaymentRequest, opts ...grpc.CallOption) (*DeletePaymentResponse, error) { out := new(DeletePaymentResponse) err := c.cc.Invoke(ctx, "/lnrpc.Lightning/DeletePayment", in, out, opts...) @@ -1644,6 +1670,14 @@ type LightningServer interface { // lncli: `listpayments` // ListPayments returns a list of all outgoing payments. ListPayments(context.Context, *ListPaymentsRequest) (*ListPaymentsResponse, error) + // lncli: `listpaymentduplicates` + // ListPaymentDuplicates returns duplicate payment records for the given + // payment hash. This RPC is only supported by the SQL payments backend. + ListPaymentDuplicates(context.Context, *ListPaymentDuplicatesRequest) (*ListPaymentDuplicatesResponse, error) + // lncli: `listallpaymentduplicates` + // ListAllPaymentDuplicates returns duplicate payment records across all + // payments. This RPC is only supported by the SQL payments backend. + ListAllPaymentDuplicates(context.Context, *ListAllPaymentDuplicatesRequest) (*ListAllPaymentDuplicatesResponse, error) // lncli: `deletepayments` // DeletePayment deletes an outgoing payment from DB. Note that it will not // attempt to delete an In-Flight payment, since that would be unsafe. @@ -1948,6 +1982,12 @@ func (UnimplementedLightningServer) DecodePayReq(context.Context, *PayReqString) func (UnimplementedLightningServer) ListPayments(context.Context, *ListPaymentsRequest) (*ListPaymentsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ListPayments not implemented") } +func (UnimplementedLightningServer) ListPaymentDuplicates(context.Context, *ListPaymentDuplicatesRequest) (*ListPaymentDuplicatesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListPaymentDuplicates not implemented") +} +func (UnimplementedLightningServer) ListAllPaymentDuplicates(context.Context, *ListAllPaymentDuplicatesRequest) (*ListAllPaymentDuplicatesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListAllPaymentDuplicates not implemented") +} func (UnimplementedLightningServer) DeletePayment(context.Context, *DeletePaymentRequest) (*DeletePaymentResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method DeletePayment not implemented") } @@ -2816,6 +2856,42 @@ func _Lightning_ListPayments_Handler(srv interface{}, ctx context.Context, dec f return interceptor(ctx, in, info, handler) } +func _Lightning_ListPaymentDuplicates_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListPaymentDuplicatesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LightningServer).ListPaymentDuplicates(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/lnrpc.Lightning/ListPaymentDuplicates", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LightningServer).ListPaymentDuplicates(ctx, req.(*ListPaymentDuplicatesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Lightning_ListAllPaymentDuplicates_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListAllPaymentDuplicatesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LightningServer).ListAllPaymentDuplicates(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/lnrpc.Lightning/ListAllPaymentDuplicates", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LightningServer).ListAllPaymentDuplicates(ctx, req.(*ListAllPaymentDuplicatesRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _Lightning_DeletePayment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(DeletePaymentRequest) if err := dec(in); err != nil { @@ -3525,6 +3601,14 @@ var Lightning_ServiceDesc = grpc.ServiceDesc{ MethodName: "ListPayments", Handler: _Lightning_ListPayments_Handler, }, + { + MethodName: "ListPaymentDuplicates", + Handler: _Lightning_ListPaymentDuplicates_Handler, + }, + { + MethodName: "ListAllPaymentDuplicates", + Handler: _Lightning_ListAllPaymentDuplicates_Handler, + }, { MethodName: "DeletePayment", Handler: _Lightning_DeletePayment_Handler, diff --git a/lnrpc/routerrpc/router_backend.go b/lnrpc/routerrpc/router_backend.go index d8c8a17c410..b729d098f4c 100644 --- a/lnrpc/routerrpc/router_backend.go +++ b/lnrpc/routerrpc/router_backend.go @@ -1767,6 +1767,10 @@ func (r *RouterBackend) MarshallPayment(payment *paymentsdb.MPPayment) ( // If any of the htlcs have settled, extract a valid // preimage. if htlc.Settle != nil { + // For AMP payments all hashes will be different so we + // will only show the last htlc preimage, this is a + // current limitation for AMP payments because for + // MPP payments all hashes are the same. preimage = htlc.Settle.Preimage fee += htlc.Route.TotalFees() } @@ -1795,7 +1799,7 @@ func (r *RouterBackend) MarshallPayment(payment *paymentsdb.MPPayment) ( paymentID := payment.Info.PaymentIdentifier creationTimeNS := MarshalTimeNano(payment.Info.CreationTime) - failureReason, err := marshallPaymentFailureReason( + failureReason, err := MarshallPaymentFailureReason( payment.FailureReason, ) if err != nil { @@ -1852,9 +1856,9 @@ func convertPaymentStatus(dbStatus paymentsdb.PaymentStatus, useInit bool) ( } } -// marshallPaymentFailureReason marshalls the failure reason to the corresponding rpc -// type. -func marshallPaymentFailureReason(reason *paymentsdb.FailureReason) ( +// MarshallPaymentFailureReason marshalls the failure reason to the +// corresponding rpc type. +func MarshallPaymentFailureReason(reason *paymentsdb.FailureReason) ( lnrpc.PaymentFailureReason, error) { if reason == nil { diff --git a/lnrpc/routerrpc/router_server.go b/lnrpc/routerrpc/router_server.go index a4031b154eb..7f2514aee51 100644 --- a/lnrpc/routerrpc/router_server.go +++ b/lnrpc/routerrpc/router_server.go @@ -1088,11 +1088,11 @@ func (s *Server) SendToRouteV2(ctx context.Context, // db. if req.SkipTempErr { attempt, err = s.cfg.Router.SendToRouteSkipTempErr( - hash, route, firstHopRecords, + ctx, hash, route, firstHopRecords, ) } else { attempt, err = s.cfg.Router.SendToRoute( - hash, route, firstHopRecords, + ctx, hash, route, firstHopRecords, ) } if attempt != nil { diff --git a/payments/db/errors.go b/payments/db/errors.go index 6d5bd211ca6..0457db60035 100644 --- a/payments/db/errors.go +++ b/payments/db/errors.go @@ -84,6 +84,12 @@ var ( ErrMixedBlindedAndNonBlindedPayments = errors.New("mixed blinded and " + "non-blinded payments") + // ErrBlindedPaymentMissingTotalAmount is returned if we try to + // register a blinded payment attempt where the final hop doesn't set + // the total amount. + ErrBlindedPaymentMissingTotalAmount = errors.New("blinded payment " + + "final hop must set total amount") + // ErrMPPPaymentAddrMismatch is returned if we try to register an MPP // shard where the payment address doesn't match existing shards. ErrMPPPaymentAddrMismatch = errors.New("payment address mismatch") @@ -136,4 +142,8 @@ var ( // NOTE: Only used for the kv backend. ErrNoSequenceNrIndex = errors.New("payment sequence number index " + "does not exist") + + // errMaxPaymentsReached is used internally to signal that the maximum + // number of payments has been reached during a paginated query. + errMaxPaymentsReached = errors.New("max payments reached") ) diff --git a/payments/db/interface.go b/payments/db/interface.go index c41dc371f89..cb3fa614ee3 100644 --- a/payments/db/interface.go +++ b/payments/db/interface.go @@ -21,21 +21,39 @@ type PaymentReader interface { // FetchPayment fetches the payment corresponding to the given payment // hash. - FetchPayment(paymentHash lntypes.Hash) (*MPPayment, error) + FetchPayment(ctx context.Context, + paymentHash lntypes.Hash) (*MPPayment, error) // FetchInFlightPayments returns all payments with status InFlight. - FetchInFlightPayments() ([]*MPPayment, error) + FetchInFlightPayments(ctx context.Context) ([]*MPPayment, error) +} + +// DuplicatePaymentsReader provides access to legacy duplicate payment records. +// This is only supported by the SQL payments backend, so callers should use a +// type assertion and handle unsupported backends. +type DuplicatePaymentsReader interface { + // FetchDuplicatePayments returns duplicate payment records for a single + // payment hash. + FetchDuplicatePayments(ctx context.Context, + paymentHash lntypes.Hash) ([]*DuplicatePayment, error) + + // FetchAllDuplicatePayments returns duplicate payment records across + // all payments. + FetchAllDuplicatePayments(ctx context.Context) ( + []*DuplicatePayment, error) } // PaymentWriter represents the interface to write operations to the payments // database. type PaymentWriter interface { // DeletePayment deletes a payment from the DB given its payment hash. - DeletePayment(paymentHash lntypes.Hash, failedAttemptsOnly bool) error + DeletePayment(ctx context.Context, paymentHash lntypes.Hash, + failedAttemptsOnly bool) error // DeletePayments deletes all payments from the DB given the specified // flags. - DeletePayments(failedOnly, failedAttemptsOnly bool) (int, error) + DeletePayments(ctx context.Context, failedOnly, + failedAttemptsOnly bool) (int, error) PaymentControl } @@ -58,10 +76,22 @@ type PaymentControl interface { // exists in the database before creating a new payment. However, it // should allow the user making a subsequent payment if the payment is // in a Failed state. - InitPayment(lntypes.Hash, *PaymentCreationInfo) error + InitPayment(context.Context, lntypes.Hash, *PaymentCreationInfo) error // RegisterAttempt atomically records the provided HTLCAttemptInfo. - RegisterAttempt(lntypes.Hash, *HTLCAttemptInfo) (*MPPayment, error) + // + // IMPORTANT: Callers MUST serialize calls to RegisterAttempt for the + // same payment hash. Concurrent calls will result in race conditions + // where both calls read the same initial payment state, validate + // against stale data, and could cause overpayment. For example: + // - Both goroutines fetch payment with 400 sats sent + // - Both validate sending 650 sats won't overpay (within limit) + // - Both commit successfully + // - Result: 1700 sats sent, exceeding the payment amount + // The payment router/controller layer is responsible for ensuring + // serialized access per payment hash. + RegisterAttempt(context.Context, lntypes.Hash, + *HTLCAttemptInfo) (*MPPayment, error) // SettleAttempt marks the given attempt settled with the preimage. If // this is a multi shard payment, this might implicitly mean the @@ -71,10 +101,12 @@ type PaymentControl interface { // error to prevent us from making duplicate payments to the same // payment hash. The provided preimage is atomically saved to the DB // for record keeping. - SettleAttempt(lntypes.Hash, uint64, *HTLCSettleInfo) (*MPPayment, error) + SettleAttempt(context.Context, lntypes.Hash, uint64, + *HTLCSettleInfo) (*MPPayment, error) // FailAttempt marks the given payment attempt failed. - FailAttempt(lntypes.Hash, uint64, *HTLCFailInfo) (*MPPayment, error) + FailAttempt(context.Context, lntypes.Hash, uint64, + *HTLCFailInfo) (*MPPayment, error) // Fail transitions a payment into the Failed state, and records // the ultimate reason the payment failed. Note that this should only @@ -82,12 +114,12 @@ type PaymentControl interface { // invoking this method, InitPayment should return nil on its next call // for this payment hash, allowing the user to make a subsequent // payment. - Fail(lntypes.Hash, FailureReason) (*MPPayment, error) + Fail(context.Context, lntypes.Hash, FailureReason) (*MPPayment, error) // DeleteFailedAttempts removes all failed HTLCs from the db. It should // be called for a given payment whenever all inflight htlcs are // completed, and the payment has reached a final terminal state. - DeleteFailedAttempts(lntypes.Hash) error + DeleteFailedAttempts(context.Context, lntypes.Hash) error } // DBMPPayment is an interface that represents the payment state during a diff --git a/payments/db/kv_store.go b/payments/db/kv_store.go index 62f0b83867e..6d21048d7e0 100644 --- a/payments/db/kv_store.go +++ b/payments/db/kv_store.go @@ -127,10 +127,6 @@ type KVStore struct { // db is the underlying database implementation. db kvdb.Backend - - // keepFailedPaymentAttempts is a flag that indicates whether we should - // keep failed payment attempts in the database. - keepFailedPaymentAttempts bool } // A compile-time constraint to ensure KVStore implements DB. @@ -152,8 +148,7 @@ func NewKVStore(db kvdb.Backend, } return &KVStore{ - db: db, - keepFailedPaymentAttempts: opts.KeepFailedPaymentAttempts, + db: db, }, nil } @@ -186,7 +181,7 @@ func initKVStore(db kvdb.Backend) error { // making sure it does not already exist as an in-flight payment. When this // method returns successfully, the payment is guaranteed to be in the InFlight // state. -func (p *KVStore) InitPayment(paymentHash lntypes.Hash, +func (p *KVStore) InitPayment(_ context.Context, paymentHash lntypes.Hash, info *PaymentCreationInfo) error { // Obtain a new sequence number for this payment. This is used @@ -288,15 +283,14 @@ func (p *KVStore) InitPayment(paymentHash lntypes.Hash, return updateErr } -// DeleteFailedAttempts deletes all failed htlcs for a payment if configured -// by the KVStore db. -func (p *KVStore) DeleteFailedAttempts(hash lntypes.Hash) error { - if !p.keepFailedPaymentAttempts { - const failedHtlcsOnly = true - err := p.DeletePayment(hash, failedHtlcsOnly) - if err != nil { - return err - } +// DeleteFailedAttempts deletes all failed htlcs for a payment. +func (p *KVStore) DeleteFailedAttempts(ctx context.Context, + hash lntypes.Hash) error { + + const failedHtlcsOnly = true + err := p.DeletePayment(ctx, hash, failedHtlcsOnly) + if err != nil { + return err } return nil @@ -357,7 +351,7 @@ func deserializePaymentIndex(r io.Reader) (lntypes.Hash, error) { // RegisterAttempt atomically records the provided HTLCAttemptInfo to the // DB. -func (p *KVStore) RegisterAttempt(paymentHash lntypes.Hash, +func (p *KVStore) RegisterAttempt(_ context.Context, paymentHash lntypes.Hash, attempt *HTLCAttemptInfo) (*MPPayment, error) { // Serialize the information before opening the db transaction. @@ -428,7 +422,7 @@ func (p *KVStore) RegisterAttempt(paymentHash lntypes.Hash, // After invoking this method, InitPayment should always return an error to // prevent us from making duplicate payments to the same payment hash. The // provided preimage is atomically saved to the DB for record keeping. -func (p *KVStore) SettleAttempt(hash lntypes.Hash, +func (p *KVStore) SettleAttempt(_ context.Context, hash lntypes.Hash, attemptID uint64, settleInfo *HTLCSettleInfo) (*MPPayment, error) { var b bytes.Buffer @@ -441,7 +435,7 @@ func (p *KVStore) SettleAttempt(hash lntypes.Hash, } // FailAttempt marks the given payment attempt failed. -func (p *KVStore) FailAttempt(hash lntypes.Hash, +func (p *KVStore) FailAttempt(_ context.Context, hash lntypes.Hash, attemptID uint64, failInfo *HTLCFailInfo) (*MPPayment, error) { var b bytes.Buffer @@ -526,7 +520,7 @@ func (p *KVStore) updateHtlcKey(paymentHash lntypes.Hash, // payment failed. After invoking this method, InitPayment should return nil on // its next call for this payment hash, allowing the switch to make a // subsequent payment. -func (p *KVStore) Fail(paymentHash lntypes.Hash, +func (p *KVStore) Fail(_ context.Context, paymentHash lntypes.Hash, reason FailureReason) (*MPPayment, error) { var ( @@ -583,8 +577,8 @@ func (p *KVStore) Fail(paymentHash lntypes.Hash, } // FetchPayment returns information about a payment from the database. -func (p *KVStore) FetchPayment(paymentHash lntypes.Hash) ( - *MPPayment, error) { +func (p *KVStore) FetchPayment(_ context.Context, + paymentHash lntypes.Hash) (*MPPayment, error) { var payment *MPPayment err := kvdb.View(p.db, func(tx kvdb.RTx) error { @@ -739,7 +733,9 @@ func fetchPaymentStatus(bucket kvdb.RBucket) (PaymentStatus, error) { } // FetchInFlightPayments returns all payments with status InFlight. -func (p *KVStore) FetchInFlightPayments() ([]*MPPayment, error) { +func (p *KVStore) FetchInFlightPayments(_ context.Context) ([]*MPPayment, + error) { + var ( inFlights []*MPPayment start = time.Now() @@ -1273,7 +1269,7 @@ func fetchPaymentWithSequenceNumber(tx kvdb.RTx, paymentHash lntypes.Hash, // DeletePayment deletes a payment from the DB given its payment hash. If // failedHtlcsOnly is set, only failed HTLC attempts of the payment will be // deleted. -func (p *KVStore) DeletePayment(paymentHash lntypes.Hash, +func (p *KVStore) DeletePayment(_ context.Context, paymentHash lntypes.Hash, failedHtlcsOnly bool) error { return kvdb.Update(p.db, func(tx kvdb.RwTx) error { @@ -1370,7 +1366,7 @@ func (p *KVStore) DeletePayment(paymentHash lntypes.Hash, // failedHtlcsOnly is set, the payment itself won't be deleted, only failed HTLC // attempts. The method returns the number of deleted payments, which is always // 0 if failedHtlcsOnly is set. -func (p *KVStore) DeletePayments(failedOnly, +func (p *KVStore) DeletePayments(_ context.Context, failedOnly, failedHtlcsOnly bool) (int, error) { var numPayments int diff --git a/payments/db/kv_store_test.go b/payments/db/kv_store_test.go index 2c2895175ad..136d3a73314 100644 --- a/payments/db/kv_store_test.go +++ b/payments/db/kv_store_test.go @@ -1,7 +1,10 @@ +//go:build !test_db_sqlite && !test_db_postgres + package paymentsdb import ( "bytes" + "crypto/sha256" "encoding/binary" "io" "math" @@ -17,221 +20,67 @@ import ( "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/tlv" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -// TestKVStoreDeleteNonInFlight checks that calling DeletePayments only -// deletes payments from the database that are not in-flight. -// -// TODO(ziggie): Make this test db agnostic. -func TestKVStoreDeleteNonInFlight(t *testing.T) { +// TestKVStoreDeleteDuplicatePayments tests that when a payment with duplicate +// payments is deleted, both the parent payment and its duplicates are properly +// removed from the payment index. This is specific to the KV store's legacy +// duplicate payment handling. +func TestKVStoreDeleteDuplicatePayments(t *testing.T) { t.Parallel() - paymentDB := NewKVTestDB(t) - - // Create a sequence number for duplicate payments that will not collide - // with the sequence numbers for the payments we create. These values - // start at 1, so 9999 is a safe bet for this test. - var duplicateSeqNr = 9999 - - payments := []struct { - failed bool - success bool - hasDuplicate bool - }{ - { - failed: true, - success: false, - hasDuplicate: false, - }, - { - failed: false, - success: true, - hasDuplicate: false, - }, - { - failed: false, - success: false, - hasDuplicate: false, - }, - { - failed: false, - success: true, - hasDuplicate: true, - }, - } - - var numSuccess, numInflight int - - for _, p := range payments { - info, attempt, preimg, err := genInfo(t) - if err != nil { - t.Fatalf("unable to generate htlc message: %v", err) - } - - // Sends base htlc message which initiate StatusInFlight. - err = paymentDB.InitPayment(info.PaymentIdentifier, info) - if err != nil { - t.Fatalf("unable to send htlc message: %v", err) - } - _, err = paymentDB.RegisterAttempt( - info.PaymentIdentifier, attempt, - ) - if err != nil { - t.Fatalf("unable to send htlc message: %v", err) - } - - htlc := &htlcStatus{ - HTLCAttemptInfo: attempt, - } - - switch { - case p.failed: - // Fail the payment attempt. - htlcFailure := HTLCFailUnreadable - _, err := paymentDB.FailAttempt( - info.PaymentIdentifier, attempt.AttemptID, - &HTLCFailInfo{ - Reason: htlcFailure, - }, - ) - if err != nil { - t.Fatalf("unable to fail htlc: %v", err) - } + ctx := t.Context() - // Fail the payment, which should moved it to Failed. - failReason := FailureReasonNoRoute - _, err = paymentDB.Fail( - info.PaymentIdentifier, failReason, - ) - if err != nil { - t.Fatalf("unable to fail payment hash: %v", err) - } - - // Verify the status is indeed Failed. - assertDBPaymentstatus( - t, paymentDB, info.PaymentIdentifier, - StatusFailed, - ) - - htlc.failure = &htlcFailure - assertPaymentInfo( - t, paymentDB, info.PaymentIdentifier, info, - &failReason, htlc, - ) - - case p.success: - // Verifies that status was changed to StatusSucceeded. - _, err := paymentDB.SettleAttempt( - info.PaymentIdentifier, attempt.AttemptID, - &HTLCSettleInfo{ - Preimage: preimg, - }, - ) - if err != nil { - t.Fatalf("error shouldn't have been received,"+ - " got: %v", err) - } - - assertDBPaymentstatus( - t, paymentDB, info.PaymentIdentifier, - StatusSucceeded, - ) - - htlc.settle = &preimg - assertPaymentInfo( - t, paymentDB, info.PaymentIdentifier, info, nil, - htlc, - ) - - numSuccess++ + paymentDB := NewKVTestDB(t) - default: - assertDBPaymentstatus( - t, paymentDB, info.PaymentIdentifier, - StatusInFlight, - ) - assertPaymentInfo( - t, paymentDB, info.PaymentIdentifier, info, nil, - htlc, - ) + // Create a successful payment. + preimg := genPreimage(t) - numInflight++ - } + rhash := sha256.Sum256(preimg[:]) + info := genPaymentCreationInfo(t, rhash) + attempt := genAttemptWithHash(t, 0, genSessionKey(t), rhash) - // If the payment is intended to have a duplicate payment, we - // add one. - if p.hasDuplicate { - appendDuplicatePayment( - t, paymentDB.db, info.PaymentIdentifier, - uint64(duplicateSeqNr), preimg, - ) - duplicateSeqNr++ - numSuccess++ - } - } + // Init and settle the payment. + err := paymentDB.InitPayment(ctx, info.PaymentIdentifier, info) + require.NoError(t, err, "unable to init payment") - // Delete all failed payments. - numPayments, err := paymentDB.DeletePayments(true, false) - require.NoError(t, err) - require.EqualValues(t, 1, numPayments) - - // This should leave the succeeded and in-flight payments. - dbPayments, err := paymentDB.FetchPayments() - if err != nil { - t.Fatal(err) - } - - if len(dbPayments) != numSuccess+numInflight { - t.Fatalf("expected %d payments, got %d", - numSuccess+numInflight, len(dbPayments)) - } + _, err = paymentDB.RegisterAttempt( + ctx, info.PaymentIdentifier, attempt, + ) + require.NoError(t, err, "unable to register attempt") - var s, i int - for _, p := range dbPayments { - t.Log("fetch payment has status", p.Status) - switch p.Status { - case StatusSucceeded: - s++ - case StatusInFlight: - i++ - } - } + _, err = paymentDB.SettleAttempt( + ctx, info.PaymentIdentifier, attempt.AttemptID, + &HTLCSettleInfo{ + Preimage: preimg, + }, + ) + require.NoError(t, err, "unable to settle attempt") - if s != numSuccess { - t.Fatalf("expected %d succeeded payments , got %d", - numSuccess, s) - } - if i != numInflight { - t.Fatalf("expected %d in-flight payments, got %d", - numInflight, i) - } + assertDBPaymentstatus( + t, paymentDB, info.PaymentIdentifier, StatusSucceeded, + ) - // Now delete all payments except in-flight. - numPayments, err = paymentDB.DeletePayments(false, false) + // Fetch the payment to get its sequence number. + payment, err := paymentDB.FetchPayment(ctx, info.PaymentIdentifier) require.NoError(t, err) - require.EqualValues(t, 2, numPayments) - // This should leave the in-flight payment. - dbPayments, err = paymentDB.FetchPayments() - if err != nil { - t.Fatal(err) - } - - if len(dbPayments) != numInflight { - t.Fatalf("expected %d payments, got %d", numInflight, - len(dbPayments)) - } + // Add two duplicate payments. Use high sequence numbers that won't + // collide with the original payment. + duplicateSeqNr1 := payment.SequenceNum + 1000 + duplicateSeqNr2 := payment.SequenceNum + 1001 - for _, p := range dbPayments { - if p.Status != StatusInFlight { - t.Fatalf("expected in-fligth status, got %v", p.Status) - } - } + appendDuplicatePayment( + t, paymentDB.db, info.PaymentIdentifier, duplicateSeqNr1, + preimg, + ) + appendDuplicatePayment( + t, paymentDB.db, info.PaymentIdentifier, duplicateSeqNr2, + preimg, + ) - // Finally, check that we only have a single index left in the payment - // index bucket. + // Verify we now have 3 index entries: original + 2 duplicates. var indexCount int err = kvdb.View(paymentDB.db, func(tx walletdb.ReadTx) error { index := tx.ReadBucket(paymentsIndexBucket) @@ -242,85 +91,33 @@ func TestKVStoreDeleteNonInFlight(t *testing.T) { }) }, func() { indexCount = 0 }) require.NoError(t, err) + require.Equal(t, 3, indexCount, "expected 3 index entries "+ + "(parent + 2 duplicates)") - require.Equal(t, 1, indexCount) -} - -type htlcStatus struct { - *HTLCAttemptInfo - settle *lntypes.Preimage - failure *HTLCFailReason -} - -// fetchPaymentIndexEntry gets the payment hash for the sequence number provided -// from our payment indexes bucket. -func fetchPaymentIndexEntry(t *testing.T, p *KVStore, - sequenceNumber uint64) (*lntypes.Hash, error) { - - t.Helper() - - var hash lntypes.Hash - - if err := kvdb.View(p.db, func(tx walletdb.ReadTx) error { - indexBucket := tx.ReadBucket(paymentsIndexBucket) - key := make([]byte, 8) - byteOrder.PutUint64(key, sequenceNumber) - - indexValue := indexBucket.Get(key) - if indexValue == nil { - return ErrNoSequenceNrIndex - } - - r := bytes.NewReader(indexValue) - - var err error - hash, err = deserializePaymentIndex(r) - - return err - }, func() { - hash = lntypes.Hash{} - }); err != nil { - return nil, err - } - - return &hash, nil -} - -// assertPaymentIndex looks up the index for a payment in the db and checks -// that its payment hash matches the expected hash passed in. -func assertPaymentIndex(t *testing.T, p DB, expectedHash lntypes.Hash) { - t.Helper() - - // Only the kv implementation uses the index so we exit early if the - // payment db is not a kv implementation. This helps us to reuse the - // same test for both implementations. - kvPaymentDB, ok := p.(*KVStore) - if !ok { - return - } - - // Lookup the payment so that we have its sequence number and check - // that is has correctly been indexed in the payment indexes bucket. - pmt, err := kvPaymentDB.FetchPayment(expectedHash) + // Delete all successful payments. + numPayments, err := paymentDB.DeletePayments(ctx, false, false) require.NoError(t, err) + require.EqualValues(t, 1, numPayments, "should delete 1 payment") - hash, err := fetchPaymentIndexEntry(t, kvPaymentDB, pmt.SequenceNum) + // Verify all payments are deleted. + dbPayments, err := paymentDB.FetchPayments() require.NoError(t, err) - assert.Equal(t, expectedHash, *hash) -} + require.Empty(t, dbPayments, "all payments should be deleted") -// assertNoIndex checks that an index for the sequence number provided does not -// exist. -func assertNoIndex(t *testing.T, p DB, seqNr uint64) { - t.Helper() - - kvPaymentDB, ok := p.(*KVStore) - if !ok { - return - } + // Verify the payment index is now empty - all 3 entries (parent + + // duplicates) should be removed. + indexCount = 0 + err = kvdb.View(paymentDB.db, func(tx walletdb.ReadTx) error { + index := tx.ReadBucket(paymentsIndexBucket) - _, err := fetchPaymentIndexEntry(t, kvPaymentDB, seqNr) - require.Equal(t, ErrNoSequenceNrIndex, err) + return index.ForEach(func(k, v []byte) error { + indexCount++ + return nil + }) + }, func() { indexCount = 0 }) + require.NoError(t, err) + require.Equal(t, 0, indexCount, "payment index should be empty "+ + "after deleting payment with duplicates") } func makeFakeInfo(t *testing.T) (*PaymentCreationInfo, @@ -477,35 +274,35 @@ func deletePayment(t *testing.T, db kvdb.Backend, paymentHash lntypes.Hash, func TestFetchPaymentWithSequenceNumber(t *testing.T) { paymentDB := NewKVTestDB(t) + ctx := t.Context() + // Generate a test payment which does not have duplicates. - noDuplicates, _, _, err := genInfo(t) - require.NoError(t, err) + noDuplicates, _ := genInfo(t) // Create a new payment entry in the database. - err = paymentDB.InitPayment( - noDuplicates.PaymentIdentifier, noDuplicates, + err := paymentDB.InitPayment( + ctx, noDuplicates.PaymentIdentifier, noDuplicates, ) require.NoError(t, err) // Fetch the payment so we can get its sequence nr. noDuplicatesPayment, err := paymentDB.FetchPayment( - noDuplicates.PaymentIdentifier, + ctx, noDuplicates.PaymentIdentifier, ) require.NoError(t, err) // Generate a test payment which we will add duplicates to. - hasDuplicates, _, preimg, err := genInfo(t) - require.NoError(t, err) + hasDuplicates, preimg := genInfo(t) // Create a new payment entry in the database. err = paymentDB.InitPayment( - hasDuplicates.PaymentIdentifier, hasDuplicates, + ctx, hasDuplicates.PaymentIdentifier, hasDuplicates, ) require.NoError(t, err) // Fetch the payment so we can get its sequence nr. hasDuplicatesPayment, err := paymentDB.FetchPayment( - hasDuplicates.PaymentIdentifier, + ctx, hasDuplicates.PaymentIdentifier, ) require.NoError(t, err) @@ -652,8 +449,7 @@ func putDuplicatePayment(t *testing.T, duplicateBucket kvdb.RwBucket, require.NoError(t, err) // Generate fake information for the duplicate payment. - info, _, _, err := genInfo(t) - require.NoError(t, err) + info, _ := genInfo(t) // Write the payment info to disk under the creation info key. This code // is copied rather than using serializePaymentCreationInfo to ensure @@ -684,17 +480,19 @@ func putDuplicatePayment(t *testing.T, duplicateBucket kvdb.RwBucket, require.NoError(t, err) } -// TestQueryPayments tests retrieval of payments with forwards and reversed -// queries. -// -// TODO(ziggie): Make this test db agnostic. -func TestQueryPayments(t *testing.T) { - // Define table driven test for QueryPayments. +// TestKVStoreQueryPaymentsDuplicates tests the KV store's legacy duplicate +// payment handling. This tests the specific case where duplicate payments +// are stored in a nested bucket within the parent payment bucket. +func TestKVStoreQueryPaymentsDuplicates(t *testing.T) { + t.Parallel() + // Test payments have sequence indices [1, 3, 4, 5, 6, 7]. // Note that the payment with index 7 has the same payment hash as 6, // and is stored in a nested bucket within payment 6 rather than being - // its own entry in the payments bucket. We do this to test retrieval - // of legacy payments. + // its own entry in the payments bucket. This tests retrieval of legacy + // duplicate payments which is KV-store specific. + // These test cases focus on validating that duplicate payments (seq 7, + // nested under payment 6) are correctly returned in queries. tests := []struct { name string query Query @@ -706,31 +504,20 @@ func TestQueryPayments(t *testing.T) { expectedSeqNrs []uint64 }{ { - name: "IndexOffset at the end of the payments range", - query: Query{ - IndexOffset: 7, - MaxPayments: 7, - Reversed: false, - IncludeIncomplete: true, - }, - firstIndex: 0, - lastIndex: 0, - expectedSeqNrs: nil, - }, - { - name: "query in forwards order, start at beginning", + name: "query includes duplicate payment in forward " + + "order", query: Query{ - IndexOffset: 0, - MaxPayments: 2, + IndexOffset: 5, + MaxPayments: 3, Reversed: false, IncludeIncomplete: true, }, - firstIndex: 1, - lastIndex: 3, - expectedSeqNrs: []uint64{1, 3}, + firstIndex: 6, + lastIndex: 7, + expectedSeqNrs: []uint64{6, 7}, }, { - name: "query in forwards order, start at end, overflow", + name: "query duplicate payment at end", query: Query{ IndexOffset: 6, MaxPayments: 2, @@ -742,44 +529,7 @@ func TestQueryPayments(t *testing.T) { expectedSeqNrs: []uint64{7}, }, { - name: "start at offset index outside of payments", - query: Query{ - IndexOffset: 20, - MaxPayments: 2, - Reversed: false, - IncludeIncomplete: true, - }, - firstIndex: 0, - lastIndex: 0, - expectedSeqNrs: nil, - }, - { - name: "overflow in forwards order", - query: Query{ - IndexOffset: 4, - MaxPayments: math.MaxUint64, - Reversed: false, - IncludeIncomplete: true, - }, - firstIndex: 5, - lastIndex: 7, - expectedSeqNrs: []uint64{5, 6, 7}, - }, - { - name: "start at offset index outside of payments, " + - "reversed order", - query: Query{ - IndexOffset: 9, - MaxPayments: 2, - Reversed: true, - IncludeIncomplete: true, - }, - firstIndex: 6, - lastIndex: 7, - expectedSeqNrs: []uint64{6, 7}, - }, - { - name: "query in reverse order, start at end", + name: "query includes duplicate in reverse order", query: Query{ IndexOffset: 0, MaxPayments: 2, @@ -791,36 +541,11 @@ func TestQueryPayments(t *testing.T) { expectedSeqNrs: []uint64{6, 7}, }, { - name: "query in reverse order, starting in middle", - query: Query{ - IndexOffset: 4, - MaxPayments: 2, - Reversed: true, - IncludeIncomplete: true, - }, - firstIndex: 1, - lastIndex: 3, - expectedSeqNrs: []uint64{1, 3}, - }, - { - name: "query in reverse order, starting in middle, " + - "with underflow", - query: Query{ - IndexOffset: 4, - MaxPayments: 5, - Reversed: true, - IncludeIncomplete: true, - }, - firstIndex: 1, - lastIndex: 3, - expectedSeqNrs: []uint64{1, 3}, - }, - { - name: "all payments in reverse, order maintained", + name: "query all payments includes duplicate", query: Query{ IndexOffset: 0, - MaxPayments: 7, - Reversed: true, + MaxPayments: math.MaxUint64, + Reversed: false, IncludeIncomplete: true, }, firstIndex: 1, @@ -828,7 +553,7 @@ func TestQueryPayments(t *testing.T) { expectedSeqNrs: []uint64{1, 3, 4, 5, 6, 7}, }, { - name: "exclude incomplete payments", + name: "exclude incomplete includes duplicate", query: Query{ IndexOffset: 0, MaxPayments: 7, @@ -839,96 +564,6 @@ func TestQueryPayments(t *testing.T) { lastIndex: 7, expectedSeqNrs: []uint64{7}, }, - { - name: "query payments at index gap", - query: Query{ - IndexOffset: 1, - MaxPayments: 7, - Reversed: false, - IncludeIncomplete: true, - }, - firstIndex: 3, - lastIndex: 7, - expectedSeqNrs: []uint64{3, 4, 5, 6, 7}, - }, - { - name: "query payments reverse before index gap", - query: Query{ - IndexOffset: 3, - MaxPayments: 7, - Reversed: true, - IncludeIncomplete: true, - }, - firstIndex: 1, - lastIndex: 1, - expectedSeqNrs: []uint64{1}, - }, - { - name: "query payments reverse on index gap", - query: Query{ - IndexOffset: 2, - MaxPayments: 7, - Reversed: true, - IncludeIncomplete: true, - }, - firstIndex: 1, - lastIndex: 1, - expectedSeqNrs: []uint64{1}, - }, - { - name: "query payments forward on index gap", - query: Query{ - IndexOffset: 2, - MaxPayments: 2, - Reversed: false, - IncludeIncomplete: true, - }, - firstIndex: 3, - lastIndex: 4, - expectedSeqNrs: []uint64{3, 4}, - }, - { - name: "query in forwards order, with start creation " + - "time", - query: Query{ - IndexOffset: 0, - MaxPayments: 2, - Reversed: false, - IncludeIncomplete: true, - CreationDateStart: 5, - }, - firstIndex: 5, - lastIndex: 6, - expectedSeqNrs: []uint64{5, 6}, - }, - { - name: "query in forwards order, with start creation " + - "time at end, overflow", - query: Query{ - IndexOffset: 0, - MaxPayments: 2, - Reversed: false, - IncludeIncomplete: true, - CreationDateStart: 7, - }, - firstIndex: 7, - lastIndex: 7, - expectedSeqNrs: []uint64{7}, - }, - { - name: "query with start and end creation time", - query: Query{ - IndexOffset: 9, - MaxPayments: math.MaxUint64, - Reversed: true, - IncludeIncomplete: true, - CreationDateStart: 3, - CreationDateEnd: 5, - }, - firstIndex: 3, - lastIndex: 5, - expectedSeqNrs: []uint64{3, 4, 5}, - }, } for _, tt := range tests { @@ -939,10 +574,6 @@ func TestQueryPayments(t *testing.T) { paymentDB := NewKVTestDB(t) - // Initialize the payment database. - paymentDB, err := NewKVStore(paymentDB.db) - require.NoError(t, err) - // Make a preliminary query to make sure it's ok to // query when we have no payments. resp, err := paymentDB.QueryPayments(ctx, tt.query) @@ -960,25 +591,22 @@ func TestQueryPayments(t *testing.T) { for i := 0; i < nonDuplicatePayments; i++ { // Generate a test payment. - info, _, preimg, err := genInfo(t) - if err != nil { - t.Fatalf("unable to create test "+ - "payment: %v", err) - } + info, preimg := genInfo(t) + // Override creation time to allow for testing // of CreationDateStart and CreationDateEnd. info.CreationTime = time.Unix(int64(i+1), 0) // Create a new payment entry in the database. err = paymentDB.InitPayment( - info.PaymentIdentifier, info, + ctx, info.PaymentIdentifier, info, ) require.NoError(t, err) // Immediately delete the payment with index 2. if i == 1 { pmt, err := paymentDB.FetchPayment( - info.PaymentIdentifier, + ctx, info.PaymentIdentifier, ) require.NoError(t, err) @@ -995,7 +623,7 @@ func TestQueryPayments(t *testing.T) { // duplicate payments will always be succeeded. if i == (nonDuplicatePayments - 1) { pmt, err := paymentDB.FetchPayment( - info.PaymentIdentifier, + ctx, info.PaymentIdentifier, ) require.NoError(t, err) diff --git a/payments/db/kv_tombstone.go b/payments/db/kv_tombstone.go new file mode 100644 index 00000000000..3f94a801f88 --- /dev/null +++ b/payments/db/kv_tombstone.go @@ -0,0 +1,67 @@ +package paymentsdb + +import ( + "fmt" + + "github.com/lightningnetwork/lnd/kvdb" +) + +var ( + // paymentsBucketTombstone is the key used to mark the payments bucket + // as permanently closed after a successful migration. + paymentsBucketTombstone = []byte("payments-tombstone") +) + +// SetPaymentsBucketTombstone sets the tombstone key in the payments bucket to +// mark the bucket as permanently closed. This prevents it from being reopened +// in the future. +func SetPaymentsBucketTombstone(db kvdb.Backend) error { + return kvdb.Update(db, func(tx kvdb.RwTx) error { + // Access the top-level payments bucket. + payments := tx.ReadWriteBucket(paymentsRootBucket) + if payments == nil { + var err error + payments, err = tx.CreateTopLevelBucket( + paymentsRootBucket, + ) + if err != nil { + return fmt.Errorf("payments bucket does not "+ + "exist: %w", err) + } + } + + // Add the tombstone key to the payments bucket. + err := payments.Put(paymentsBucketTombstone, []byte("1")) + if err != nil { + return fmt.Errorf("failed to set tombstone: %w", err) + } + + return nil + }, func() {}) +} + +// GetPaymentsBucketTombstone checks if the tombstone key exists in the payments +// bucket. It returns true if the tombstone is present and false otherwise. +func GetPaymentsBucketTombstone(db kvdb.Backend) (bool, error) { + var tombstoneExists bool + + err := kvdb.View(db, func(tx kvdb.RTx) error { + // Access the top-level payments bucket. + payments := tx.ReadBucket(paymentsRootBucket) + if payments == nil { + tombstoneExists = false + return nil + } + + // Check if the tombstone key exists. + tombstone := payments.Get(paymentsBucketTombstone) + tombstoneExists = tombstone != nil + + return nil + }, func() {}) + if err != nil { + return false, err + } + + return tombstoneExists, nil +} diff --git a/payments/db/log.go b/payments/db/log.go index 8a77dbcec7f..c8892341da7 100644 --- a/payments/db/log.go +++ b/payments/db/log.go @@ -3,6 +3,7 @@ package paymentsdb import ( "github.com/btcsuite/btclog/v2" "github.com/lightningnetwork/lnd/build" + paymentsmig1 "github.com/lightningnetwork/lnd/payments/db/migration1" ) // log is a logger that is initialized with no output filters. This @@ -29,4 +30,5 @@ func DisableLog() { // using btclog. func UseLogger(logger btclog.Logger) { log = logger + paymentsmig1.UseLogger(logger) } diff --git a/payments/db/migration1/codec.go b/payments/db/migration1/codec.go new file mode 100644 index 00000000000..926fb66d181 --- /dev/null +++ b/payments/db/migration1/codec.go @@ -0,0 +1,141 @@ +package migration1 + +import ( + "encoding/binary" + "errors" + "io" + "time" + + "github.com/lightningnetwork/lnd/channeldb" +) + +// Big endian is the preferred byte order, due to cursor scans over +// integer keys iterating in order. +var byteOrder = binary.BigEndian + +// UnknownElementType is an alias for channeldb.UnknownElementType. +type UnknownElementType = channeldb.UnknownElementType + +// ReadElement deserializes a single element from the provided io.Reader. +func ReadElement(r io.Reader, element interface{}) error { + err := channeldb.ReadElement(r, element) + switch { + // Known to channeldb codec. + case err == nil: + return nil + + // Fail if error is not UnknownElementType. + default: + var unknownElementType UnknownElementType + if !errors.As(err, &unknownElementType) { + return err + } + } + + // Process any paymentsdb-specific extensions to the codec. + switch e := element.(type) { + case *paymentIndexType: + if err := binary.Read(r, byteOrder, e); err != nil { + return err + } + + // Type is still unknown to paymentsdb extensions, fail. + default: + return channeldb.NewUnknownElementType( + "ReadElement", element, + ) + } + + return nil +} + +// WriteElement serializes a single element into the provided io.Writer. +func WriteElement(w io.Writer, element interface{}) error { + err := channeldb.WriteElement(w, element) + switch { + // Known to channeldb codec. + case err == nil: + return nil + + // Fail if error is not UnknownElementType. + default: + var unknownElementType UnknownElementType + if !errors.As(err, &unknownElementType) { + return err + } + } + + // Process any paymentsdb-specific extensions to the codec. + switch e := element.(type) { + case paymentIndexType: + if err := binary.Write(w, byteOrder, e); err != nil { + return err + } + + // Type is still unknown to paymentsdb extensions, fail. + default: + return channeldb.NewUnknownElementType( + "WriteElement", element, + ) + } + + return nil +} + +// WriteElements serializes a variadic list of elements into the given +// io.Writer. +func WriteElements(w io.Writer, elements ...interface{}) error { + for _, element := range elements { + if err := WriteElement(w, element); err != nil { + return err + } + } + + return nil +} + +// ReadElements deserializes the provided io.Reader into a variadic list of +// target elements. +func ReadElements(r io.Reader, elements ...interface{}) error { + for _, element := range elements { + if err := ReadElement(r, element); err != nil { + return err + } + } + + return nil +} + +// deserializeTime deserializes time as unix nanoseconds. +func deserializeTime(r io.Reader) (time.Time, error) { + var scratch [8]byte + if _, err := io.ReadFull(r, scratch[:]); err != nil { + return time.Time{}, err + } + + // Convert to time.Time. Interpret unix nano time zero as a zero + // time.Time value. + unixNano := byteOrder.Uint64(scratch[:]) + if unixNano == 0 { + return time.Time{}, nil + } + + return time.Unix(0, int64(unixNano)), nil +} + +// serializeTime serializes time as unix nanoseconds. +func serializeTime(w io.Writer, t time.Time) error { + var scratch [8]byte + + // Convert to unix nano seconds, but only if time is non-zero. Calling + // UnixNano() on a zero time yields an undefined result. + var unixNano int64 + if !t.IsZero() { + unixNano = t.UnixNano() + } + + byteOrder.PutUint64(scratch[:], uint64(unixNano)) + _, err := w.Write(scratch[:]) + + return err +} diff --git a/payments/db/migration1/errors.go b/payments/db/migration1/errors.go new file mode 100644 index 00000000000..44c3981e84b --- /dev/null +++ b/payments/db/migration1/errors.go @@ -0,0 +1,149 @@ +package migration1 + +import "errors" + +var ( + // ErrAlreadyPaid signals we have already paid this payment hash. + ErrAlreadyPaid = errors.New("invoice is already paid") + + // ErrPaymentInFlight signals that payment for this payment hash is + // already "in flight" on the network. + ErrPaymentInFlight = errors.New("payment is in transition") + + // ErrPaymentExists is returned when we try to initialize an already + // existing payment that is not failed. + ErrPaymentExists = errors.New("payment already exists") + + // ErrPaymentInternal is returned when performing the payment has a + // conflicting state, such as, + // - payment has StatusSucceeded but remaining amount is not zero. + // - payment has StatusInitiated but remaining amount is zero. + // - payment has StatusFailed but remaining amount is zero. + ErrPaymentInternal = errors.New("internal error") + + // ErrPaymentNotInitiated is returned if the payment wasn't initiated. + ErrPaymentNotInitiated = errors.New("payment isn't initiated") + + // ErrPaymentAlreadySucceeded is returned in the event we attempt to + // change the status of a payment already succeeded. + ErrPaymentAlreadySucceeded = errors.New("payment is already succeeded") + + // ErrPaymentAlreadyFailed is returned in the event we attempt to alter + // a failed payment. + ErrPaymentAlreadyFailed = errors.New("payment has already failed") + + // ErrUnknownPaymentStatus is returned when we do not recognize the + // existing state of a payment. + ErrUnknownPaymentStatus = errors.New("unknown payment status") + + // ErrPaymentTerminal is returned if we attempt to alter a payment that + // already has reached a terminal condition. + ErrPaymentTerminal = errors.New("payment has reached terminal " + + "condition") + + // ErrAttemptAlreadySettled is returned if we try to alter an already + // settled HTLC attempt. + ErrAttemptAlreadySettled = errors.New("attempt already settled") + + // ErrAttemptAlreadyFailed is returned if we try to alter an already + // failed HTLC attempt. + ErrAttemptAlreadyFailed = errors.New("attempt already failed") + + // ErrValueMismatch is returned if we try to register a non-MPP attempt + // with an amount that doesn't match the payment amount. + ErrValueMismatch = errors.New("attempted value doesn't match payment " + + "amount") + + // ErrValueExceedsAmt is returned if we try to register an attempt that + // would take the total sent amount above the payment amount. + ErrValueExceedsAmt = errors.New("attempted value exceeds payment " + + "amount") + + // ErrNonMPPayment is returned if we try to register an MPP attempt for + // a payment that already has a non-MPP attempt registered. + ErrNonMPPayment = errors.New("payment has non-MPP attempts") + + // ErrMPPayment is returned if we try to register a non-MPP attempt for + // a payment that already has an MPP attempt registered. + ErrMPPayment = errors.New("payment has MPP attempts") + + // ErrMPPRecordInBlindedPayment is returned if we try to register an + // attempt with an MPP record for a payment to a blinded path. + ErrMPPRecordInBlindedPayment = errors.New("blinded payment cannot " + + "contain MPP records") + + // ErrBlindedPaymentTotalAmountMismatch is returned if we try to + // register an HTLC shard to a blinded route where the total amount + // doesn't match existing shards. + ErrBlindedPaymentTotalAmountMismatch = errors.New("blinded path " + + "total amount mismatch") + + // ErrMixedBlindedAndNonBlindedPayments is returned if we try to + // register a non-blinded attempt to a payment which uses a blinded + // paths or vice versa. + ErrMixedBlindedAndNonBlindedPayments = errors.New("mixed blinded and " + + "non-blinded payments") + + // ErrBlindedPaymentMissingTotalAmount is returned if we try to + // register a blinded payment attempt where the final hop doesn't set + // the total amount. + ErrBlindedPaymentMissingTotalAmount = errors.New("blinded payment " + + "final hop must set total amount") + + // ErrMPPPaymentAddrMismatch is returned if we try to register an MPP + // shard where the payment address doesn't match existing shards. + ErrMPPPaymentAddrMismatch = errors.New("payment address mismatch") + + // ErrMPPTotalAmountMismatch is returned if we try to register an MPP + // shard where the total amount doesn't match existing shards. + ErrMPPTotalAmountMismatch = errors.New("mp payment total amount " + + "mismatch") + + // ErrPaymentPendingSettled is returned when we try to add a new + // attempt to a payment that has at least one of its HTLCs settled. + ErrPaymentPendingSettled = errors.New("payment has settled htlcs") + + // ErrPaymentPendingFailed is returned when we try to add a new attempt + // to a payment that already has a failure reason. + ErrPaymentPendingFailed = errors.New("payment has failure reason") + + // ErrSentExceedsTotal is returned if the payment's current total sent + // amount exceed the total amount. + ErrSentExceedsTotal = errors.New("total sent exceeds total amount") + + // ErrNoAttemptInfo is returned when no attempt info is stored yet. + ErrNoAttemptInfo = errors.New("unable to find attempt info for " + + "inflight payment") +) + +// KV backend specific errors. +var ( + // ErrNoSequenceNumber is returned if we look up a payment which does + // not have a sequence number. + ErrNoSequenceNumber = errors.New("sequence number not found") + + // ErrDuplicateNotFound is returned when we lookup a payment by its + // index and cannot find a payment with a matching sequence number. + ErrDuplicateNotFound = errors.New("duplicate payment not found") + + // ErrNoDuplicateBucket is returned when we expect to find duplicates + // when looking up a payment from its index, but the payment does not + // have any. + ErrNoDuplicateBucket = errors.New("expected duplicate bucket") + + // ErrNoDuplicateNestedBucket is returned if we do not find duplicate + // payments in their own sub-bucket. + ErrNoDuplicateNestedBucket = errors.New("nested duplicate bucket not " + + "found") + + // ErrNoSequenceNrIndex is returned when an attempt to lookup a payment + // index is made for a sequence number that is not indexed. + // + // NOTE: Only used for the kv backend. + ErrNoSequenceNrIndex = errors.New("payment sequence number index " + + "does not exist") + + // errMaxPaymentsReached is used internally to signal that the maximum + // number of payments has been reached during a paginated query. + errMaxPaymentsReached = errors.New("max payments reached") +) diff --git a/payments/db/migration1/interface.go b/payments/db/migration1/interface.go new file mode 100644 index 00000000000..7d47118e928 --- /dev/null +++ b/payments/db/migration1/interface.go @@ -0,0 +1,140 @@ +package migration1 + +import ( + "context" + + "github.com/lightningnetwork/lnd/lntypes" +) + +// DB represents the interface to the underlying payments database. +type DB interface { + PaymentReader + PaymentWriter +} + +// PaymentReader represents the interface to read operations from the payments +// database. +type PaymentReader interface { + // QueryPayments queries the payments database and should support + // pagination. + QueryPayments(ctx context.Context, query Query) (Response, error) + + // FetchPayment fetches the payment corresponding to the given payment + // hash. + FetchPayment(ctx context.Context, + paymentHash lntypes.Hash) (*MPPayment, error) + + // FetchInFlightPayments returns all payments with status InFlight. + FetchInFlightPayments(ctx context.Context) ([]*MPPayment, error) +} + +// PaymentWriter represents the interface to write operations to the payments +// database. +type PaymentWriter interface { + // DeletePayment deletes a payment from the DB given its payment hash. + DeletePayment(ctx context.Context, paymentHash lntypes.Hash, + failedAttemptsOnly bool) error + + // DeletePayments deletes all payments from the DB given the specified + // flags. + DeletePayments(ctx context.Context, failedOnly, + failedAttemptsOnly bool) (int, error) + + PaymentControl +} + +// PaymentControl represents the interface to control the payment lifecycle and +// its database operations. This interface represents the control flow of how +// a payment should be handled in the database. They are not just writing +// operations but they inherently represent the flow of a payment. The methods +// are called in the following order. +// +// 1. InitPayment. +// 2. RegisterAttempt (a payment can have multiple attempts). +// 3. SettleAttempt or FailAttempt (attempts can also fail as long as the +// sending amount will be eventually settled). +// 4. Payment succeeds or "Fail" is called. +// 5. DeleteFailedAttempts is called which will delete all failed attempts +// for a payment to clean up the database. +type PaymentControl interface { + // InitPayment checks that no other payment with the same payment hash + // exists in the database before creating a new payment. However, it + // should allow the user making a subsequent payment if the payment is + // in a Failed state. + InitPayment(context.Context, lntypes.Hash, *PaymentCreationInfo) error + + // RegisterAttempt atomically records the provided HTLCAttemptInfo. + // + // IMPORTANT: Callers MUST serialize calls to RegisterAttempt for the + // same payment hash. Concurrent calls will result in race conditions + // where both calls read the same initial payment state, validate + // against stale data, and could cause overpayment. For example: + // - Both goroutines fetch payment with 400 sats sent + // - Both validate sending 650 sats won't overpay (within limit) + // - Both commit successfully + // - Result: 1700 sats sent, exceeding the payment amount + // The payment router/controller layer is responsible for ensuring + // serialized access per payment hash. + RegisterAttempt(context.Context, lntypes.Hash, + *HTLCAttemptInfo) (*MPPayment, error) + + // SettleAttempt marks the given attempt settled with the preimage. If + // this is a multi shard payment, this might implicitly mean the + // full payment succeeded. + // + // After invoking this method, InitPayment should always return an + // error to prevent us from making duplicate payments to the same + // payment hash. The provided preimage is atomically saved to the DB + // for record keeping. + SettleAttempt(context.Context, lntypes.Hash, uint64, + *HTLCSettleInfo) (*MPPayment, error) + + // FailAttempt marks the given payment attempt failed. + FailAttempt(context.Context, lntypes.Hash, uint64, + *HTLCFailInfo) (*MPPayment, error) + + // Fail transitions a payment into the Failed state, and records + // the ultimate reason the payment failed. Note that this should only + // be called when all active attempts are already failed. After + // invoking this method, InitPayment should return nil on its next call + // for this payment hash, allowing the user to make a subsequent + // payment. + Fail(context.Context, lntypes.Hash, FailureReason) (*MPPayment, error) + + // DeleteFailedAttempts removes all failed HTLCs from the db. It should + // be called for a given payment whenever all inflight htlcs are + // completed, and the payment has reached a final terminal state. + DeleteFailedAttempts(context.Context, lntypes.Hash) error +} + +// DBMPPayment is an interface that represents the payment state during a +// payment lifecycle. +type DBMPPayment interface { + // GetState returns the current state of the payment. + GetState() *MPPaymentState + + // Terminated returns true if the payment is in a final state. + Terminated() bool + + // GetStatus returns the current status of the payment. + GetStatus() PaymentStatus + + // NeedWaitAttempts specifies whether the payment needs to wait for the + // outcome of an attempt. + NeedWaitAttempts() (bool, error) + + // GetHTLCs returns all HTLCs of this payment. + GetHTLCs() []HTLCAttempt + + // InFlightHTLCs returns all HTLCs that are in flight. + InFlightHTLCs() []HTLCAttempt + + // AllowMoreAttempts is used to decide whether we can safely attempt + // more HTLCs for a given payment state. Return an error if the payment + // is in an unexpected state. + AllowMoreAttempts() (bool, error) + + // TerminalInfo returns the settled HTLC attempt or the payment's + // failure reason. + TerminalInfo() (*HTLCAttempt, *FailureReason) +} diff --git a/payments/db/migration1/kv_duplicate_payments.go b/payments/db/migration1/kv_duplicate_payments.go new file mode 100644 index 00000000000..2c7026766fc --- /dev/null +++ b/payments/db/migration1/kv_duplicate_payments.go @@ -0,0 +1,250 @@ +package migration1 + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + "time" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/lightningnetwork/lnd/kvdb" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" +) + +var ( + // duplicatePaymentsBucket is the name of a optional sub-bucket within + // the payment hash bucket, that is used to hold duplicate payments to a + // payment hash. This is needed to support information from earlier + // versions of lnd, where it was possible to pay to a payment hash more + // than once. + duplicatePaymentsBucket = []byte("payment-duplicate-bucket") + + // duplicatePaymentSettleInfoKey is a key used in the payment's + // sub-bucket to store the settle info of the payment. + duplicatePaymentSettleInfoKey = []byte("payment-settle-info") + + // duplicatePaymentAttemptInfoKey is a key used in the payment's + // sub-bucket to store the info about the latest attempt that was done + // for the payment in question. + duplicatePaymentAttemptInfoKey = []byte("payment-attempt-info") + + // duplicatePaymentCreationInfoKey is a key used in the payment's + // sub-bucket to store the creation info of the payment. + duplicatePaymentCreationInfoKey = []byte("payment-creation-info") + + // duplicatePaymentFailInfoKey is a key used in the payment's sub-bucket + // to store information about the reason a payment failed. + duplicatePaymentFailInfoKey = []byte("payment-fail-info") + + // duplicatePaymentSequenceKey is a key used in the payment's sub-bucket + // to store the sequence number of the payment. + duplicatePaymentSequenceKey = []byte("payment-sequence-key") +) + +// duplicateHTLCAttemptInfo contains static information about a specific HTLC +// attempt for a payment. This information is used by the router to handle any +// errors coming back after an attempt is made, and to query the switch about +// the status of the attempt. +type duplicateHTLCAttemptInfo struct { + // attemptID is the unique ID used for this attempt. + attemptID uint64 + + // sessionKey is the ephemeral key used for this attempt. + sessionKey [btcec.PrivKeyBytesLen]byte + + // route is the route attempted to send the HTLC. + route route.Route +} + +// fetchDuplicatePaymentStatus fetches the payment status of the payment. If +// the payment isn't found, it will return error `ErrPaymentNotInitiated`. +func fetchDuplicatePaymentStatus(bucket kvdb.RBucket) (PaymentStatus, error) { + if bucket.Get(duplicatePaymentSettleInfoKey) != nil { + return StatusSucceeded, nil + } + + if bucket.Get(duplicatePaymentFailInfoKey) != nil { + return StatusFailed, nil + } + + if bucket.Get(duplicatePaymentCreationInfoKey) != nil { + return StatusInFlight, nil + } + + return 0, ErrPaymentNotInitiated +} + +func deserializeDuplicateHTLCAttemptInfo(r io.Reader) ( + *duplicateHTLCAttemptInfo, error) { + + a := &duplicateHTLCAttemptInfo{} + err := ReadElements(r, &a.attemptID, &a.sessionKey) + if err != nil { + return nil, err + } + a.route, err = DeserializeRoute(r) + if err != nil { + return nil, err + } + + return a, nil +} + +func deserializeDuplicatePaymentCreationInfo(r io.Reader) ( + *PaymentCreationInfo, error) { + + var scratch [8]byte + + c := &PaymentCreationInfo{} + + if _, err := io.ReadFull(r, c.PaymentIdentifier[:]); err != nil { + return nil, err + } + + if _, err := io.ReadFull(r, scratch[:]); err != nil { + return nil, err + } + c.Value = lnwire.MilliSatoshi(byteOrder.Uint64(scratch[:])) + + if _, err := io.ReadFull(r, scratch[:]); err != nil { + return nil, err + } + c.CreationTime = time.Unix(int64(byteOrder.Uint64(scratch[:])), 0) + + if _, err := io.ReadFull(r, scratch[:4]); err != nil { + return nil, err + } + + reqLen := byteOrder.Uint32(scratch[:4]) + payReq := make([]byte, reqLen) + if reqLen > 0 { + if _, err := io.ReadFull(r, payReq); err != nil { + return nil, err + } + } + c.PaymentRequest = payReq + + return c, nil +} + +func fetchDuplicatePayment(bucket kvdb.RBucket) (*MPPayment, error) { + seqBytes := bucket.Get(duplicatePaymentSequenceKey) + if seqBytes == nil { + return nil, fmt.Errorf("sequence number not found") + } + + sequenceNum := binary.BigEndian.Uint64(seqBytes) + + // Get the payment status. + paymentStatus, err := fetchDuplicatePaymentStatus(bucket) + if err != nil { + return nil, err + } + + // Get the PaymentCreationInfo. + b := bucket.Get(duplicatePaymentCreationInfoKey) + if b == nil { + return nil, fmt.Errorf("creation info not found") + } + + r := bytes.NewReader(b) + creationInfo, err := deserializeDuplicatePaymentCreationInfo(r) + if err != nil { + return nil, err + } + + // Get failure reason if available. + var failureReason *FailureReason + b = bucket.Get(duplicatePaymentFailInfoKey) + if b != nil { + reason := FailureReason(b[0]) + failureReason = &reason + } + + payment := &MPPayment{ + SequenceNum: sequenceNum, + Info: creationInfo, + FailureReason: failureReason, + Status: paymentStatus, + } + + // Get the HTLCAttemptInfo. It can be absent. + b = bucket.Get(duplicatePaymentAttemptInfoKey) + if b != nil { + r = bytes.NewReader(b) + attempt, err := deserializeDuplicateHTLCAttemptInfo(r) + if err != nil { + return nil, err + } + + htlc := HTLCAttempt{ + HTLCAttemptInfo: HTLCAttemptInfo{ + AttemptID: attempt.attemptID, + Route: attempt.route, + sessionKey: attempt.sessionKey, + }, + } + + // Get the payment preimage. This is only found for + // successful payments. + b = bucket.Get(duplicatePaymentSettleInfoKey) + if b != nil { + var preimg lntypes.Preimage + copy(preimg[:], b) + + htlc.Settle = &HTLCSettleInfo{ + Preimage: preimg, + SettleTime: time.Time{}, + } + } else { + // Otherwise the payment must have failed. + htlc.Failure = &HTLCFailInfo{ + FailTime: time.Time{}, + } + } + + payment.HTLCs = []HTLCAttempt{htlc} + } + + return payment, nil +} + +func fetchDuplicatePayments(paymentHashBucket kvdb.RBucket) ([]*MPPayment, + error) { + + var payments []*MPPayment + + // For older versions of lnd, duplicate payments to a payment has was + // possible. These will be found in a sub-bucket indexed by their + // sequence number if available. + dup := paymentHashBucket.NestedReadBucket(duplicatePaymentsBucket) + if dup == nil { + return nil, nil + } + + err := dup.ForEach(func(k, v []byte) error { + subBucket := dup.NestedReadBucket(k) + if subBucket == nil { + // We one bucket for each duplicate to be found. + return fmt.Errorf("non bucket element" + + "in duplicate bucket") + } + + p, err := fetchDuplicatePayment(subBucket) + if err != nil { + return err + } + + payments = append(payments, p) + + return nil + }) + if err != nil { + return nil, err + } + + return payments, nil +} diff --git a/payments/db/migration1/kv_store.go b/payments/db/migration1/kv_store.go new file mode 100644 index 00000000000..1ec54b7fbcb --- /dev/null +++ b/payments/db/migration1/kv_store.go @@ -0,0 +1,2121 @@ +package migration1 + +import ( + "bytes" + "context" + "encoding/binary" + "errors" + "fmt" + "io" + "math" + "sort" + "sync" + "time" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/kvdb" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/record" + "github.com/lightningnetwork/lnd/routing/route" + "github.com/lightningnetwork/lnd/tlv" +) + +const ( + // paymentSeqBlockSize is the block size used when we batch allocate + // payment sequences for future payments. + paymentSeqBlockSize = 1000 + + // paymentProgressLogInterval is the interval we use limiting the + // logging output of payment processing. + paymentProgressLogInterval = 30 * time.Second +) + +//nolint:ll +var ( + // paymentsRootBucket is the name of the top-level bucket within the + // database that stores all data related to payments. Within this + // bucket, each payment hash its own sub-bucket keyed by its payment + // hash. + // + // Bucket hierarchy: + // + // root-bucket + // | + // |-- + // | |--sequence-key: + // | |--creation-info-key: + // | |--fail-info-key: <(optional) fail info> + // | | + // | |--payment-htlcs-bucket (shard-bucket) + // | | | + // | | |-- ai: + // | | |-- si: <(optional) settle info> + // | | |-- fi: <(optional) fail info> + // | | | + // | | ... + // | | + // | | + // | |--duplicate-bucket (only for old, completed payments) + // | | + // | |-- + // | | |--sequence-key: + // | | |--creation-info-key: + // | | |--ai: + // | | |--si: + // | | |--fi: + // | | + // | |-- + // | | | + // | ... ... + // | + // |-- + // | | + // | ... + // ... + // + paymentsRootBucket = []byte("payments-root-bucket") + + // paymentSequenceKey is a key used in the payment's sub-bucket to + // store the sequence number of the payment. + paymentSequenceKey = []byte("payment-sequence-key") + + // paymentCreationInfoKey is a key used in the payment's sub-bucket to + // store the creation info of the payment. + paymentCreationInfoKey = []byte("payment-creation-info") + + // paymentHtlcsBucket is a bucket where we'll store the information + // about the HTLCs that were attempted for a payment. + paymentHtlcsBucket = []byte("payment-htlcs-bucket") + + // htlcAttemptInfoKey is the key used as the prefix of an HTLC attempt + // to store the info about the attempt that was done for the HTLC in + // question. The HTLC attempt ID is concatenated at the end. + htlcAttemptInfoKey = []byte("ai") + + // htlcSettleInfoKey is the key used as the prefix of an HTLC attempt + // settle info, if any. The HTLC attempt ID is concatenated at the end. + htlcSettleInfoKey = []byte("si") + + // htlcFailInfoKey is the key used as the prefix of an HTLC attempt + // failure information, if any.The HTLC attempt ID is concatenated at + // the end. + htlcFailInfoKey = []byte("fi") + + // paymentFailInfoKey is a key used in the payment's sub-bucket to + // store information about the reason a payment failed. + paymentFailInfoKey = []byte("payment-fail-info") + + // paymentsIndexBucket is the name of the top-level bucket within the + // database that stores an index of payment sequence numbers to its + // payment hash. + // payments-sequence-index-bucket + // |--: + // |--... + // |--: + paymentsIndexBucket = []byte("payments-index-bucket") +) + +// KVStore implements persistence for payments and payment attempts. +type KVStore struct { + // Sequence management for the kv store. + seqMu sync.Mutex + currSeq uint64 + storedSeq uint64 + + // db is the underlying database implementation. + db kvdb.Backend +} + +// A compile-time constraint to ensure KVStore implements DB. +var _ DB = (*KVStore)(nil) + +// NewKVStore creates a new KVStore for payments. +func NewKVStore(db kvdb.Backend, + options ...OptionModifier) (*KVStore, error) { + + opts := DefaultOptions() + for _, applyOption := range options { + applyOption(opts) + } + + if !opts.NoMigration { + if err := initKVStore(db); err != nil { + return nil, err + } + } + + return &KVStore{ + db: db, + }, nil +} + +// paymentsTopLevelBuckets is a list of top-level buckets that are used for +// the payments database when using the kv store. +var paymentsTopLevelBuckets = [][]byte{ + paymentsRootBucket, + paymentsIndexBucket, +} + +// initKVStore creates and initializes the top-level buckets for the payment db. +func initKVStore(db kvdb.Backend) error { + err := kvdb.Update(db, func(tx kvdb.RwTx) error { + for _, tlb := range paymentsTopLevelBuckets { + if _, err := tx.CreateTopLevelBucket(tlb); err != nil { + return err + } + } + + return nil + }, func() {}) + if err != nil { + return fmt.Errorf("unable to create new payments db: %w", err) + } + + return nil +} + +// InitPayment checks or records the given PaymentCreationInfo with the DB, +// making sure it does not already exist as an in-flight payment. When this +// method returns successfully, the payment is guaranteed to be in the InFlight +// state. +func (p *KVStore) InitPayment(_ context.Context, paymentHash lntypes.Hash, + info *PaymentCreationInfo) error { + + // Obtain a new sequence number for this payment. This is used + // to sort the payments in order of creation, and also acts as + // a unique identifier for each payment. + sequenceNum, err := p.nextPaymentSequence() + if err != nil { + return err + } + + var b bytes.Buffer + if err := serializePaymentCreationInfo(&b, info); err != nil { + return err + } + infoBytes := b.Bytes() + + var updateErr error + err = kvdb.Batch(p.db, func(tx kvdb.RwTx) error { + // Reset the update error, to avoid carrying over an error + // from a previous execution of the batched db transaction. + updateErr = nil + + prefetchPayment(tx, paymentHash) + bucket, err := createPaymentBucket(tx, paymentHash) + if err != nil { + return err + } + + // Get the existing status of this payment, if any. + paymentStatus, err := fetchPaymentStatus(bucket) + + switch { + // If no error is returned, it means we already have this + // payment. We'll check the status to decide whether we allow + // retrying the payment or return a specific error. + case err == nil: + if err := paymentStatus.initializable(); err != nil { + updateErr = err + return nil + } + + // Otherwise, if the error is not `ErrPaymentNotInitiated`, + // we'll return the error. + case !errors.Is(err, ErrPaymentNotInitiated): + return err + } + + // Before we set our new sequence number, we check whether this + // payment has a previously set sequence number and remove its + // index entry if it exists. This happens in the case where we + // have a previously attempted payment which was left in a state + // where we can retry. + seqBytes := bucket.Get(paymentSequenceKey) + if seqBytes != nil { + indexBucket := tx.ReadWriteBucket(paymentsIndexBucket) + if err := indexBucket.Delete(seqBytes); err != nil { + return err + } + } + + // Once we have obtained a sequence number, we add an entry + // to our index bucket which will map the sequence number to + // our payment identifier. + err = createPaymentIndexEntry( + tx, sequenceNum, info.PaymentIdentifier, + ) + if err != nil { + return err + } + + err = bucket.Put(paymentSequenceKey, sequenceNum) + if err != nil { + return err + } + + // Add the payment info to the bucket, which contains the + // static information for this payment + err = bucket.Put(paymentCreationInfoKey, infoBytes) + if err != nil { + return err + } + + // We'll delete any lingering HTLCs to start with, in case we + // are initializing a payment that was attempted earlier, but + // left in a state where we could retry. + err = bucket.DeleteNestedBucket(paymentHtlcsBucket) + if err != nil && !errors.Is(err, kvdb.ErrBucketNotFound) { + return err + } + + // Also delete any lingering failure info now that we are + // re-attempting. + return bucket.Delete(paymentFailInfoKey) + }) + if err != nil { + return fmt.Errorf("unable to init payment: %w", err) + } + + return updateErr +} + +// DeleteFailedAttempts deletes all failed htlcs for a payment. +func (p *KVStore) DeleteFailedAttempts(ctx context.Context, + hash lntypes.Hash) error { + + const failedHtlcsOnly = true + err := p.DeletePayment(ctx, hash, failedHtlcsOnly) + if err != nil { + return err + } + + return nil +} + +// paymentIndexTypeHash is a payment index type which indicates that we have +// created an index of payment sequence number to payment hash. +type paymentIndexType uint8 + +// paymentIndexTypeHash is a payment index type which indicates that we have +// created an index of payment sequence number to payment hash. +const paymentIndexTypeHash paymentIndexType = 0 + +// createPaymentIndexEntry creates a payment hash typed index for a payment. The +// index produced contains a payment index type (which can be used in future to +// signal different payment index types) and the payment identifier. +func createPaymentIndexEntry(tx kvdb.RwTx, sequenceNumber []byte, + id lntypes.Hash) error { + + var b bytes.Buffer + if err := WriteElements(&b, paymentIndexTypeHash, id[:]); err != nil { + return err + } + + indexes := tx.ReadWriteBucket(paymentsIndexBucket) + + return indexes.Put(sequenceNumber, b.Bytes()) +} + +// deserializePaymentIndex deserializes a payment index entry. This function +// currently only supports deserialization of payment hash indexes, and will +// fail for other types. +func deserializePaymentIndex(r io.Reader) (lntypes.Hash, error) { + var ( + indexType paymentIndexType + paymentHash []byte + ) + + if err := ReadElements(r, &indexType, &paymentHash); err != nil { + return lntypes.Hash{}, err + } + + // While we only have on payment index type, we do not need to use our + // index type to deserialize the index. However, we sanity check that + // this type is as expected, since we had to read it out anyway. + if indexType != paymentIndexTypeHash { + return lntypes.Hash{}, fmt.Errorf("unknown payment index "+ + "type: %v", indexType) + } + + hash, err := lntypes.MakeHash(paymentHash) + if err != nil { + return lntypes.Hash{}, err + } + + return hash, nil +} + +// RegisterAttempt atomically records the provided HTLCAttemptInfo to the +// DB. +func (p *KVStore) RegisterAttempt(_ context.Context, paymentHash lntypes.Hash, + attempt *HTLCAttemptInfo) (*MPPayment, error) { + + // Serialize the information before opening the db transaction. + var a bytes.Buffer + err := serializeHTLCAttemptInfo(&a, attempt) + if err != nil { + return nil, err + } + htlcInfoBytes := a.Bytes() + + htlcIDBytes := make([]byte, 8) + binary.BigEndian.PutUint64(htlcIDBytes, attempt.AttemptID) + + var payment *MPPayment + err = kvdb.Batch(p.db, func(tx kvdb.RwTx) error { + prefetchPayment(tx, paymentHash) + bucket, err := fetchPaymentBucketUpdate(tx, paymentHash) + if err != nil { + return err + } + + payment, err = fetchPayment(bucket) + if err != nil { + return err + } + + // Check if registering a new attempt is allowed. + if err := payment.Registrable(); err != nil { + return err + } + + // Verify the attempt is compatible with the existing payment. + if err := verifyAttempt(payment, attempt); err != nil { + return err + } + + htlcsBucket, err := bucket.CreateBucketIfNotExists( + paymentHtlcsBucket, + ) + if err != nil { + return err + } + + err = htlcsBucket.Put( + htlcBucketKey(htlcAttemptInfoKey, htlcIDBytes), + htlcInfoBytes, + ) + if err != nil { + return err + } + + // Retrieve attempt info for the notification. + payment, err = fetchPayment(bucket) + + return err + }) + if err != nil { + return nil, err + } + + return payment, err +} + +// SettleAttempt marks the given attempt settled with the preimage. If this is +// a multi shard payment, this might implicitly mean that the full payment +// succeeded. +// +// After invoking this method, InitPayment should always return an error to +// prevent us from making duplicate payments to the same payment hash. The +// provided preimage is atomically saved to the DB for record keeping. +func (p *KVStore) SettleAttempt(_ context.Context, hash lntypes.Hash, + attemptID uint64, settleInfo *HTLCSettleInfo) (*MPPayment, error) { + + var b bytes.Buffer + if err := serializeHTLCSettleInfo(&b, settleInfo); err != nil { + return nil, err + } + settleBytes := b.Bytes() + + return p.updateHtlcKey(hash, attemptID, htlcSettleInfoKey, settleBytes) +} + +// FailAttempt marks the given payment attempt failed. +func (p *KVStore) FailAttempt(_ context.Context, hash lntypes.Hash, + attemptID uint64, failInfo *HTLCFailInfo) (*MPPayment, error) { + + var b bytes.Buffer + if err := serializeHTLCFailInfo(&b, failInfo); err != nil { + return nil, err + } + failBytes := b.Bytes() + + return p.updateHtlcKey(hash, attemptID, htlcFailInfoKey, failBytes) +} + +// updateHtlcKey updates a database key for the specified htlc. +func (p *KVStore) updateHtlcKey(paymentHash lntypes.Hash, + attemptID uint64, key, value []byte) (*MPPayment, error) { + + aid := make([]byte, 8) + binary.BigEndian.PutUint64(aid, attemptID) + + var payment *MPPayment + err := kvdb.Batch(p.db, func(tx kvdb.RwTx) error { + payment = nil + + prefetchPayment(tx, paymentHash) + bucket, err := fetchPaymentBucketUpdate(tx, paymentHash) + if err != nil { + return err + } + + p, err := fetchPayment(bucket) + if err != nil { + return err + } + + // We can only update keys of in-flight payments. We allow + // updating keys even if the payment has reached a terminal + // condition, since the HTLC outcomes must still be updated. + if err := p.Status.updatable(); err != nil { + return err + } + + htlcsBucket := bucket.NestedReadWriteBucket(paymentHtlcsBucket) + if htlcsBucket == nil { + return fmt.Errorf("htlcs bucket not found") + } + + attemptKey := htlcBucketKey(htlcAttemptInfoKey, aid) + if htlcsBucket.Get(attemptKey) == nil { + return fmt.Errorf("HTLC with ID %v not registered", + attemptID) + } + + // Make sure the shard is not already failed or settled. + failKey := htlcBucketKey(htlcFailInfoKey, aid) + if htlcsBucket.Get(failKey) != nil { + return ErrAttemptAlreadyFailed + } + + settleKey := htlcBucketKey(htlcSettleInfoKey, aid) + if htlcsBucket.Get(settleKey) != nil { + return ErrAttemptAlreadySettled + } + + // Add or update the key for this htlc. + err = htlcsBucket.Put(htlcBucketKey(key, aid), value) + if err != nil { + return err + } + + // Retrieve attempt info for the notification. + payment, err = fetchPayment(bucket) + + return err + }) + if err != nil { + return nil, err + } + + return payment, err +} + +// Fail transitions a payment into the Failed state, and records the reason the +// payment failed. After invoking this method, InitPayment should return nil on +// its next call for this payment hash, allowing the switch to make a +// subsequent payment. +func (p *KVStore) Fail(_ context.Context, paymentHash lntypes.Hash, + reason FailureReason) (*MPPayment, error) { + + var ( + updateErr error + payment *MPPayment + ) + err := kvdb.Batch(p.db, func(tx kvdb.RwTx) error { + // Reset the update error, to avoid carrying over an error + // from a previous execution of the batched db transaction. + updateErr = nil + payment = nil + + prefetchPayment(tx, paymentHash) + bucket, err := fetchPaymentBucketUpdate(tx, paymentHash) + if errors.Is(err, ErrPaymentNotInitiated) { + updateErr = ErrPaymentNotInitiated + return nil + } else if err != nil { + return err + } + + // We mark the payment as failed as long as it is known. This + // lets the last attempt to fail with a terminal write its + // failure to the KVStore without synchronizing with + // other attempts. + _, err = fetchPaymentStatus(bucket) + if errors.Is(err, ErrPaymentNotInitiated) { + updateErr = ErrPaymentNotInitiated + return nil + } else if err != nil { + return err + } + + // Put the failure reason in the bucket for record keeping. + v := []byte{byte(reason)} + err = bucket.Put(paymentFailInfoKey, v) + if err != nil { + return err + } + + // Retrieve attempt info for the notification, if available. + payment, err = fetchPayment(bucket) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return nil, err + } + + return payment, updateErr +} + +// FetchPayment returns information about a payment from the database. +func (p *KVStore) FetchPayment(_ context.Context, + paymentHash lntypes.Hash) (*MPPayment, error) { + + var payment *MPPayment + err := kvdb.View(p.db, func(tx kvdb.RTx) error { + prefetchPayment(tx, paymentHash) + bucket, err := fetchPaymentBucket(tx, paymentHash) + if err != nil { + return err + } + + payment, err = fetchPayment(bucket) + + return err + }, func() { + payment = nil + }) + if err != nil { + return nil, err + } + + return payment, nil +} + +// prefetchPayment attempts to prefetch as much of the payment as possible to +// reduce DB roundtrips. +func prefetchPayment(tx kvdb.RTx, paymentHash lntypes.Hash) { + rb := kvdb.RootBucket(tx) + kvdb.Prefetch( + rb, + []string{ + // Prefetch all keys in the payment's bucket. + string(paymentsRootBucket), + string(paymentHash[:]), + }, + []string{ + // Prefetch all keys in the payment's htlc bucket. + string(paymentsRootBucket), + string(paymentHash[:]), + string(paymentHtlcsBucket), + }, + ) +} + +// createPaymentBucket creates or fetches the sub-bucket assigned to this +// payment hash. +func createPaymentBucket(tx kvdb.RwTx, paymentHash lntypes.Hash) ( + kvdb.RwBucket, error) { + + payments, err := tx.CreateTopLevelBucket(paymentsRootBucket) + if err != nil { + return nil, err + } + + return payments.CreateBucketIfNotExists(paymentHash[:]) +} + +// fetchPaymentBucket fetches the sub-bucket assigned to this payment hash. If +// the bucket does not exist, it returns ErrPaymentNotInitiated. +func fetchPaymentBucket(tx kvdb.RTx, paymentHash lntypes.Hash) ( + kvdb.RBucket, error) { + + payments := tx.ReadBucket(paymentsRootBucket) + if payments == nil { + return nil, ErrPaymentNotInitiated + } + + bucket := payments.NestedReadBucket(paymentHash[:]) + if bucket == nil { + return nil, ErrPaymentNotInitiated + } + + return bucket, nil +} + +// fetchPaymentBucketUpdate is identical to fetchPaymentBucket, but it returns a +// bucket that can be written to. +func fetchPaymentBucketUpdate(tx kvdb.RwTx, paymentHash lntypes.Hash) ( + kvdb.RwBucket, error) { + + payments := tx.ReadWriteBucket(paymentsRootBucket) + if payments == nil { + return nil, ErrPaymentNotInitiated + } + + bucket := payments.NestedReadWriteBucket(paymentHash[:]) + if bucket == nil { + return nil, ErrPaymentNotInitiated + } + + return bucket, nil +} + +// nextPaymentSequence returns the next sequence number to store for a new +// payment. +func (p *KVStore) nextPaymentSequence() ([]byte, error) { + p.seqMu.Lock() + defer p.seqMu.Unlock() + + // Set a new upper bound in the DB every 1000 payments to avoid + // conflicts on the sequence when using etcd. + if p.currSeq == p.storedSeq { + var currPaymentSeq, newUpperBound uint64 + if err := kvdb.Update(p.db, func(tx kvdb.RwTx) error { + paymentsBucket, err := tx.CreateTopLevelBucket( + paymentsRootBucket, + ) + if err != nil { + return err + } + + currPaymentSeq = paymentsBucket.Sequence() + newUpperBound = currPaymentSeq + paymentSeqBlockSize + + return paymentsBucket.SetSequence(newUpperBound) + }, func() {}); err != nil { + return nil, err + } + + // We lazy initialize the cached currPaymentSeq here using the + // first nextPaymentSequence() call. This if statement will auto + // initialize our stored currPaymentSeq, since by default both + // this variable and storedPaymentSeq are zero which in turn + // will have us fetch the current values from the DB. + if p.currSeq == 0 { + p.currSeq = currPaymentSeq + } + + p.storedSeq = newUpperBound + } + + p.currSeq++ + b := make([]byte, 8) + binary.BigEndian.PutUint64(b, p.currSeq) + + return b, nil +} + +// fetchPaymentStatus fetches the payment status of the payment. If the payment +// isn't found, it will return error `ErrPaymentNotInitiated`. +func fetchPaymentStatus(bucket kvdb.RBucket) (PaymentStatus, error) { + // Creation info should be set for all payments, regardless of state. + // If not, it is unknown. + if bucket.Get(paymentCreationInfoKey) == nil { + return 0, ErrPaymentNotInitiated + } + + payment, err := fetchPayment(bucket) + if err != nil { + return 0, err + } + + return payment.Status, nil +} + +// FetchInFlightPayments returns all payments with status InFlight. +func (p *KVStore) FetchInFlightPayments(_ context.Context) ([]*MPPayment, + error) { + + var ( + inFlights []*MPPayment + start = time.Now() + lastLogTime = time.Now() + processedCount int + ) + + err := kvdb.View(p.db, func(tx kvdb.RTx) error { + payments := tx.ReadBucket(paymentsRootBucket) + if payments == nil { + return nil + } + + return payments.ForEach(func(k, _ []byte) error { + bucket := payments.NestedReadBucket(k) + if bucket == nil { + return fmt.Errorf("non bucket element") + } + + p, err := fetchPayment(bucket) + if err != nil { + return err + } + + processedCount++ + if time.Since(lastLogTime) >= + paymentProgressLogInterval { + + log.Debugf("Scanning inflight payments "+ + "(in progress), processed %d, last "+ + "processed payment: %v", processedCount, + p.Info) + + lastLogTime = time.Now() + } + + // Skip the payment if it's terminated. + if p.Terminated() { + return nil + } + + inFlights = append(inFlights, p) + + return nil + }) + }, func() { + inFlights = nil + }) + if err != nil { + return nil, err + } + + elapsed := time.Since(start) + log.Debugf("Completed scanning for inflight payments: "+ + "total_processed=%d, found_inflight=%d, elapsed=%v", + processedCount, len(inFlights), + elapsed.Round(time.Millisecond)) + + return inFlights, nil +} + +// htlcBucketKey creates a composite key from prefix and id where the result is +// simply the two concatenated. +func htlcBucketKey(prefix, id []byte) []byte { + key := make([]byte, len(prefix)+len(id)) + copy(key, prefix) + copy(key[len(prefix):], id) + + return key +} + +// FetchPayments returns all sent payments found in the DB. +func (p *KVStore) FetchPayments() ([]*MPPayment, error) { + var payments []*MPPayment + + err := kvdb.View(p.db, func(tx kvdb.RTx) error { + paymentsBucket := tx.ReadBucket(paymentsRootBucket) + if paymentsBucket == nil { + return nil + } + + return paymentsBucket.ForEach(func(k, v []byte) error { + bucket := paymentsBucket.NestedReadBucket(k) + if bucket == nil { + // We only expect sub-buckets to be found in + // this top-level bucket. + return fmt.Errorf("non bucket element in " + + "payments bucket") + } + + p, err := fetchPayment(bucket) + if err != nil { + return err + } + + payments = append(payments, p) + + // For older versions of lnd, duplicate payments to a + // payment has was possible. These will be found in a + // sub-bucket indexed by their sequence number if + // available. + duplicatePayments, err := fetchDuplicatePayments(bucket) + if err != nil { + return err + } + + payments = append(payments, duplicatePayments...) + + return nil + }) + }, func() { + payments = nil + }) + if err != nil { + return nil, err + } + + // Before returning, sort the payments by their sequence number. + sort.Slice(payments, func(i, j int) bool { + return payments[i].SequenceNum < payments[j].SequenceNum + }) + + return payments, nil +} + +func fetchCreationInfo(bucket kvdb.RBucket) (*PaymentCreationInfo, error) { + b := bucket.Get(paymentCreationInfoKey) + if b == nil { + return nil, fmt.Errorf("creation info not found") + } + + r := bytes.NewReader(b) + + return deserializePaymentCreationInfo(r) +} + +func fetchPayment(bucket kvdb.RBucket) (*MPPayment, error) { + seqBytes := bucket.Get(paymentSequenceKey) + if seqBytes == nil { + return nil, fmt.Errorf("sequence number not found") + } + + sequenceNum := binary.BigEndian.Uint64(seqBytes) + + // Get the PaymentCreationInfo. + creationInfo, err := fetchCreationInfo(bucket) + if err != nil { + return nil, err + } + + var htlcs []HTLCAttempt + htlcsBucket := bucket.NestedReadBucket(paymentHtlcsBucket) + if htlcsBucket != nil { + // Get the payment attempts. This can be empty. + htlcs, err = fetchHtlcAttempts(htlcsBucket) + if err != nil { + return nil, err + } + } + + // Get failure reason if available. + var failureReason *FailureReason + b := bucket.Get(paymentFailInfoKey) + if b != nil { + reason := FailureReason(b[0]) + failureReason = &reason + } + + // Create a new payment. + payment := &MPPayment{ + SequenceNum: sequenceNum, + Info: creationInfo, + HTLCs: htlcs, + FailureReason: failureReason, + } + + // Set its state and status. + if err := payment.setState(); err != nil { + return nil, err + } + + return payment, nil +} + +// fetchHtlcAttempts retrieves all htlc attempts made for the payment found in +// the given bucket. +func fetchHtlcAttempts(bucket kvdb.RBucket) ([]HTLCAttempt, error) { + htlcsMap := make(map[uint64]*HTLCAttempt) + + attemptInfoCount := 0 + err := bucket.ForEach(func(k, v []byte) error { + aid := byteOrder.Uint64(k[len(k)-8:]) + + if _, ok := htlcsMap[aid]; !ok { + htlcsMap[aid] = &HTLCAttempt{} + } + + var err error + switch { + case bytes.HasPrefix(k, htlcAttemptInfoKey): + attemptInfo, err := readHtlcAttemptInfo(v) + if err != nil { + return err + } + + attemptInfo.AttemptID = aid + htlcsMap[aid].HTLCAttemptInfo = *attemptInfo + attemptInfoCount++ + + case bytes.HasPrefix(k, htlcSettleInfoKey): + htlcsMap[aid].Settle, err = readHtlcSettleInfo(v) + if err != nil { + return err + } + + case bytes.HasPrefix(k, htlcFailInfoKey): + htlcsMap[aid].Failure, err = readHtlcFailInfo(v) + if err != nil { + return err + } + + default: + return fmt.Errorf("unknown htlc attempt key") + } + + return nil + }) + if err != nil { + return nil, err + } + + // Sanity check that all htlcs have an attempt info. + if attemptInfoCount != len(htlcsMap) { + return nil, ErrNoAttemptInfo + } + + keys := make([]uint64, len(htlcsMap)) + i := 0 + for k := range htlcsMap { + keys[i] = k + i++ + } + + // Sort HTLC attempts by their attempt ID. This is needed because in the + // DB we store the attempts with keys prefixed by their status which + // changes order (groups them together by status). + sort.Slice(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + + htlcs := make([]HTLCAttempt, len(htlcsMap)) + for i, key := range keys { + htlcs[i] = *htlcsMap[key] + } + + return htlcs, nil +} + +// readHtlcAttemptInfo reads the payment attempt info for this htlc. +func readHtlcAttemptInfo(b []byte) (*HTLCAttemptInfo, error) { + r := bytes.NewReader(b) + return deserializeHTLCAttemptInfo(r) +} + +// readHtlcSettleInfo reads the settle info for the htlc. If the htlc isn't +// settled, nil is returned. +func readHtlcSettleInfo(b []byte) (*HTLCSettleInfo, error) { + r := bytes.NewReader(b) + return deserializeHTLCSettleInfo(r) +} + +// readHtlcFailInfo reads the failure info for the htlc. If the htlc hasn't +// failed, nil is returned. +func readHtlcFailInfo(b []byte) (*HTLCFailInfo, error) { + r := bytes.NewReader(b) + return deserializeHTLCFailInfo(r) +} + +// fetchFailedHtlcKeys retrieves the bucket keys of all failed HTLCs of a +// payment bucket. +func fetchFailedHtlcKeys(bucket kvdb.RBucket) ([][]byte, error) { + htlcsBucket := bucket.NestedReadBucket(paymentHtlcsBucket) + + var htlcs []HTLCAttempt + var err error + if htlcsBucket != nil { + htlcs, err = fetchHtlcAttempts(htlcsBucket) + if err != nil { + return nil, err + } + } + + // Now iterate though them and save the bucket keys for the failed + // HTLCs. + var htlcKeys [][]byte + for _, h := range htlcs { + if h.Failure == nil { + continue + } + + htlcKeyBytes := make([]byte, 8) + binary.BigEndian.PutUint64(htlcKeyBytes, h.AttemptID) + + htlcKeys = append(htlcKeys, htlcKeyBytes) + } + + return htlcKeys, nil +} + +// QueryPayments is a query to the payments database which is restricted +// to a subset of payments by the payments query, containing an offset +// index and a maximum number of returned payments. +func (p *KVStore) QueryPayments(_ context.Context, + query Query) (Response, error) { + + var resp Response + + if err := kvdb.View(p.db, func(tx kvdb.RTx) error { + // Get the root payments bucket. + paymentsBucket := tx.ReadBucket(paymentsRootBucket) + if paymentsBucket == nil { + return nil + } + + // Get the index bucket which maps sequence number -> payment + // hash and duplicate bool. If we have a payments bucket, we + // should have an indexes bucket as well. + indexes := tx.ReadBucket(paymentsIndexBucket) + if indexes == nil { + return fmt.Errorf("index bucket does not exist") + } + + // accumulatePayments gets payments with the sequence number + // and hash provided and adds them to our list of payments if + // they meet the criteria of our query. It returns the number + // of payments that were added. + accumulatePayments := func(sequenceKey, hash []byte) (bool, + error) { + + r := bytes.NewReader(hash) + paymentHash, err := deserializePaymentIndex(r) + if err != nil { + return false, err + } + + payment, err := fetchPaymentWithSequenceNumber( + tx, paymentHash, sequenceKey, + ) + if err != nil { + return false, err + } + + // To keep compatibility with the old API, we only + // return non-succeeded payments if requested. + if payment.Status != StatusSucceeded && + !query.IncludeIncomplete { + + return false, err + } + + // Get the creation time in Unix seconds, this always + // rounds down the nanoseconds to full seconds. + createTime := payment.Info.CreationTime.Unix() + + // Skip any payments that were created before the + // specified time. + if createTime < query.CreationDateStart { + return false, nil + } + + // Skip any payments that were created after the + // specified time. + if query.CreationDateEnd != 0 && + createTime > query.CreationDateEnd { + + return false, nil + } + + // At this point, we've exhausted the offset, so we'll + // begin collecting invoices found within the range. + resp.Payments = append(resp.Payments, payment) + + return true, nil + } + + // Create a paginator which reads from our sequence index bucket + // with the parameters provided by the payments query. + paginator := channeldb.NewPaginator( + indexes.ReadCursor(), query.Reversed, query.IndexOffset, + query.MaxPayments, + ) + + // Run a paginated query, adding payments to our response. + if err := paginator.Query(accumulatePayments); err != nil { + return err + } + + // Counting the total number of payments is expensive, since we + // literally have to traverse the cursor linearly, which can + // take quite a while. So it's an optional query parameter. + if query.CountTotal { + var ( + totalPayments uint64 + err error + ) + countFn := func(_, _ []byte) error { + totalPayments++ + + return nil + } + + // In non-boltdb database backends, there's a faster + // ForAll query that allows for batch fetching items. + fastBucket, ok := indexes.(kvdb.ExtendedRBucket) + if ok { + err = fastBucket.ForAll(countFn) + } else { + err = indexes.ForEach(countFn) + } + if err != nil { + return fmt.Errorf("error counting payments: %w", + err) + } + + resp.TotalCount = totalPayments + } + + return nil + }, func() { + resp = Response{} + }); err != nil { + return resp, err + } + + // Need to swap the payments slice order if reversed order. + if query.Reversed { + for l, r := 0, len(resp.Payments)-1; l < r; l, r = l+1, r-1 { + resp.Payments[l], resp.Payments[r] = + resp.Payments[r], resp.Payments[l] + } + } + + // Set the first and last index of the returned payments so that the + // caller can resume from this point later on. + if len(resp.Payments) > 0 { + resp.FirstIndexOffset = resp.Payments[0].SequenceNum + resp.LastIndexOffset = + resp.Payments[len(resp.Payments)-1].SequenceNum + } + + return resp, nil +} + +// fetchPaymentWithSequenceNumber get the payment which matches the payment hash +// *and* sequence number provided from the database. This is required because +// we previously had more than one payment per hash, so we have multiple indexes +// pointing to a single payment; we want to retrieve the correct one. +func fetchPaymentWithSequenceNumber(tx kvdb.RTx, paymentHash lntypes.Hash, + sequenceNumber []byte) (*MPPayment, error) { + + // We can now lookup the payment keyed by its hash in + // the payments root bucket. + bucket, err := fetchPaymentBucket(tx, paymentHash) + if err != nil { + return nil, err + } + + // A single payment hash can have multiple payments associated with it. + // We lookup our sequence number first, to determine whether this is + // the payment we are actually looking for. + seqBytes := bucket.Get(paymentSequenceKey) + if seqBytes == nil { + return nil, ErrNoSequenceNumber + } + + // If this top level payment has the sequence number we are looking for, + // return it. + if bytes.Equal(seqBytes, sequenceNumber) { + return fetchPayment(bucket) + } + + // If we were not looking for the top level payment, we are looking for + // one of our duplicate payments. We need to iterate through the seq + // numbers in this bucket to find the correct payments. If we do not + // find a duplicate payments bucket here, something is wrong. + dup := bucket.NestedReadBucket(duplicatePaymentsBucket) + if dup == nil { + return nil, ErrNoDuplicateBucket + } + + var duplicatePayment *MPPayment + err = dup.ForEach(func(k, v []byte) error { + subBucket := dup.NestedReadBucket(k) + if subBucket == nil { + // We one bucket for each duplicate to be found. + return ErrNoDuplicateNestedBucket + } + + seqBytes := subBucket.Get(duplicatePaymentSequenceKey) + if seqBytes == nil { + return err + } + + // If this duplicate payment is not the sequence number we are + // looking for, we can continue. + if !bytes.Equal(seqBytes, sequenceNumber) { + return nil + } + + duplicatePayment, err = fetchDuplicatePayment(subBucket) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return nil, err + } + + // If none of the duplicate payments matched our sequence number, we + // failed to find the payment with this sequence number; something is + // wrong. + if duplicatePayment == nil { + return nil, ErrDuplicateNotFound + } + + return duplicatePayment, nil +} + +// DeletePayment deletes a payment from the DB given its payment hash. If +// failedHtlcsOnly is set, only failed HTLC attempts of the payment will be +// deleted. +func (p *KVStore) DeletePayment(_ context.Context, paymentHash lntypes.Hash, + failedHtlcsOnly bool) error { + + return kvdb.Update(p.db, func(tx kvdb.RwTx) error { + payments := tx.ReadWriteBucket(paymentsRootBucket) + if payments == nil { + return nil + } + + bucket := payments.NestedReadWriteBucket(paymentHash[:]) + if bucket == nil { + return fmt.Errorf("non bucket element in payments " + + "bucket") + } + + // If the status is InFlight, we cannot safely delete + // the payment information, so we return early. + paymentStatus, err := fetchPaymentStatus(bucket) + if err != nil { + return err + } + + // If the payment has inflight HTLCs, we cannot safely delete + // the payment information, so we return an error. + if err := paymentStatus.removable(); err != nil { + return fmt.Errorf("payment '%v' has inflight HTLCs"+ + "and therefore cannot be deleted: %w", + paymentHash.String(), err) + } + + // Delete the failed HTLC attempts we found. + if failedHtlcsOnly { + toDelete, err := fetchFailedHtlcKeys(bucket) + if err != nil { + return err + } + + htlcsBucket := bucket.NestedReadWriteBucket( + paymentHtlcsBucket, + ) + + for _, htlcID := range toDelete { + err = htlcsBucket.Delete( + htlcBucketKey( + htlcAttemptInfoKey, htlcID, + ), + ) + if err != nil { + return err + } + + err = htlcsBucket.Delete( + htlcBucketKey(htlcFailInfoKey, htlcID), + ) + if err != nil { + return err + } + + err = htlcsBucket.Delete( + htlcBucketKey( + htlcSettleInfoKey, htlcID, + ), + ) + if err != nil { + return err + } + } + + return nil + } + + seqNrs, err := fetchSequenceNumbers(bucket) + if err != nil { + return err + } + + err = payments.DeleteNestedBucket(paymentHash[:]) + if err != nil { + return err + } + + indexBucket := tx.ReadWriteBucket(paymentsIndexBucket) + for _, k := range seqNrs { + if err := indexBucket.Delete(k); err != nil { + return err + } + } + + return nil + }, func() {}) +} + +// DeletePayments deletes all completed and failed payments from the DB. If +// failedOnly is set, only failed payments will be considered for deletion. If +// failedHtlcsOnly is set, the payment itself won't be deleted, only failed HTLC +// attempts. The method returns the number of deleted payments, which is always +// 0 if failedHtlcsOnly is set. +func (p *KVStore) DeletePayments(_ context.Context, failedOnly, + failedHtlcsOnly bool) (int, error) { + + var numPayments int + err := kvdb.Update(p.db, func(tx kvdb.RwTx) error { + payments := tx.ReadWriteBucket(paymentsRootBucket) + if payments == nil { + return nil + } + + var ( + // deleteBuckets is the set of payment buckets we need + // to delete. + deleteBuckets [][]byte + + // deleteIndexes is the set of indexes pointing to these + // payments that need to be deleted. + deleteIndexes [][]byte + + // deleteHtlcs maps a payment hash to the HTLC IDs we + // want to delete for that payment. + deleteHtlcs = make(map[lntypes.Hash][][]byte) + ) + err := payments.ForEach(func(k, _ []byte) error { + bucket := payments.NestedReadBucket(k) + if bucket == nil { + // We only expect sub-buckets to be found in + // this top-level bucket. + return fmt.Errorf("non bucket element in " + + "payments bucket") + } + + // If the status is InFlight, we cannot safely delete + // the payment information, so we return early. + paymentStatus, err := fetchPaymentStatus(bucket) + if err != nil { + return err + } + + // If the payment has inflight HTLCs, we cannot safely + // delete the payment information, so we return an nil + // to skip it. + if err := paymentStatus.removable(); err != nil { + return nil + } + + // If we requested to only delete failed payments, we + // can return if this one is not. + if failedOnly && paymentStatus != StatusFailed { + return nil + } + + // If we are only deleting failed HTLCs, fetch them. + if failedHtlcsOnly { + toDelete, err := fetchFailedHtlcKeys(bucket) + if err != nil { + return err + } + + hash, err := lntypes.MakeHash(k) + if err != nil { + return err + } + + deleteHtlcs[hash] = toDelete + + // We return, we are only deleting attempts. + return nil + } + + // Add the bucket to the set of buckets we can delete. + deleteBuckets = append(deleteBuckets, k) + + // Get all the sequence number associated with the + // payment, including duplicates. + seqNrs, err := fetchSequenceNumbers(bucket) + if err != nil { + return err + } + + deleteIndexes = append(deleteIndexes, seqNrs...) + numPayments++ + + return nil + }) + if err != nil { + return err + } + + // Delete the failed HTLC attempts we found. + for hash, htlcIDs := range deleteHtlcs { + bucket := payments.NestedReadWriteBucket(hash[:]) + htlcsBucket := bucket.NestedReadWriteBucket( + paymentHtlcsBucket, + ) + + for _, aid := range htlcIDs { + if err := htlcsBucket.Delete( + htlcBucketKey(htlcAttemptInfoKey, aid), + ); err != nil { + return err + } + + if err := htlcsBucket.Delete( + htlcBucketKey(htlcFailInfoKey, aid), + ); err != nil { + return err + } + + if err := htlcsBucket.Delete( + htlcBucketKey(htlcSettleInfoKey, aid), + ); err != nil { + return err + } + } + } + + for _, k := range deleteBuckets { + if err := payments.DeleteNestedBucket(k); err != nil { + return err + } + } + + // Get our index bucket and delete all indexes pointing to the + // payments we are deleting. + indexBucket := tx.ReadWriteBucket(paymentsIndexBucket) + for _, k := range deleteIndexes { + if err := indexBucket.Delete(k); err != nil { + return err + } + } + + return nil + }, func() { + numPayments = 0 + }) + if err != nil { + return 0, err + } + + return numPayments, nil +} + +// fetchSequenceNumbers fetches all the sequence numbers associated with a +// payment, including those belonging to any duplicate payments. +func fetchSequenceNumbers(paymentBucket kvdb.RBucket) ([][]byte, error) { + seqNum := paymentBucket.Get(paymentSequenceKey) + if seqNum == nil { + return nil, errors.New("expected sequence number") + } + + sequenceNumbers := [][]byte{seqNum} + + // Get the duplicate payments bucket, if it has no duplicates, just + // return early with the payment sequence number. + duplicates := paymentBucket.NestedReadBucket(duplicatePaymentsBucket) + if duplicates == nil { + return sequenceNumbers, nil + } + + // If we do have duplicated, they are keyed by sequence number, so we + // iterate through the duplicates bucket and add them to our set of + // sequence numbers. + if err := duplicates.ForEach(func(k, v []byte) error { + sequenceNumbers = append(sequenceNumbers, k) + return nil + }); err != nil { + return nil, err + } + + return sequenceNumbers, nil +} + +func serializePaymentCreationInfo(w io.Writer, c *PaymentCreationInfo) error { + var scratch [8]byte + + if _, err := w.Write(c.PaymentIdentifier[:]); err != nil { + return err + } + + byteOrder.PutUint64(scratch[:], uint64(c.Value)) + if _, err := w.Write(scratch[:]); err != nil { + return err + } + + if err := serializeTime(w, c.CreationTime); err != nil { + return err + } + + byteOrder.PutUint32(scratch[:4], uint32(len(c.PaymentRequest))) + if _, err := w.Write(scratch[:4]); err != nil { + return err + } + + if _, err := w.Write(c.PaymentRequest); err != nil { + return err + } + + // Any remaining bytes are TLV encoded records. Currently, these are + // only the custom records provided by the user to be sent to the first + // hop. But this can easily be extended with further records by merging + // the records into a single TLV stream. + err := c.FirstHopCustomRecords.SerializeTo(w) + if err != nil { + return err + } + + return nil +} + +func deserializePaymentCreationInfo(r io.Reader) (*PaymentCreationInfo, + error) { + + var scratch [8]byte + + c := &PaymentCreationInfo{} + + if _, err := io.ReadFull(r, c.PaymentIdentifier[:]); err != nil { + return nil, err + } + + if _, err := io.ReadFull(r, scratch[:]); err != nil { + return nil, err + } + c.Value = lnwire.MilliSatoshi(byteOrder.Uint64(scratch[:])) + + creationTime, err := deserializeTime(r) + if err != nil { + return nil, err + } + c.CreationTime = creationTime + + if _, err := io.ReadFull(r, scratch[:4]); err != nil { + return nil, err + } + + reqLen := byteOrder.Uint32(scratch[:4]) + payReq := make([]byte, reqLen) + if reqLen > 0 { + if _, err := io.ReadFull(r, payReq); err != nil { + return nil, err + } + } + c.PaymentRequest = payReq + + // Any remaining bytes are TLV encoded records. Currently, these are + // only the custom records provided by the user to be sent to the first + // hop. But this can easily be extended with further records by merging + // the records into a single TLV stream. + c.FirstHopCustomRecords, err = lnwire.ParseCustomRecordsFrom(r) + if err != nil { + return nil, err + } + + return c, nil +} + +func serializeHTLCAttemptInfo(w io.Writer, a *HTLCAttemptInfo) error { + if err := WriteElements(w, a.sessionKey); err != nil { + return err + } + + if err := SerializeRoute(w, a.Route); err != nil { + return err + } + + if err := serializeTime(w, a.AttemptTime); err != nil { + return err + } + + // If the hash is nil we can just return. + if a.Hash == nil { + return nil + } + + if _, err := w.Write(a.Hash[:]); err != nil { + return err + } + + // Merge the fixed/known records together with the custom records to + // serialize them as a single blob. We can't do this in SerializeRoute + // because we're in the middle of the byte stream there. We can only do + // TLV serialization at the end of the stream, since EOF is allowed for + // a stream if no more data is expected. + producers := []tlv.RecordProducer{ + &a.Route.FirstHopAmount, + } + tlvData, err := lnwire.MergeAndEncode( + producers, nil, a.Route.FirstHopWireCustomRecords, + ) + if err != nil { + return err + } + + if _, err := w.Write(tlvData); err != nil { + return err + } + + return nil +} + +func deserializeHTLCAttemptInfo(r io.Reader) (*HTLCAttemptInfo, error) { + a := &HTLCAttemptInfo{} + err := ReadElements(r, &a.sessionKey) + if err != nil { + return nil, err + } + + a.Route, err = DeserializeRoute(r) + if err != nil { + return nil, err + } + + a.AttemptTime, err = deserializeTime(r) + if err != nil { + return nil, err + } + + hash := lntypes.Hash{} + _, err = io.ReadFull(r, hash[:]) + + switch { + // Older payment attempts wouldn't have the hash set, in which case we + // can just return. + case errors.Is(err, io.EOF), errors.Is(err, io.ErrUnexpectedEOF): + return a, nil + + case err != nil: + return nil, err + + default: + } + + a.Hash = &hash + + // Read any remaining data (if any) and parse it into the known records + // and custom records. + extraData, err := io.ReadAll(r) + if err != nil { + return nil, err + } + + customRecords, _, _, err := lnwire.ParseAndExtractCustomRecords( + extraData, &a.Route.FirstHopAmount, + ) + if err != nil { + return nil, err + } + + a.Route.FirstHopWireCustomRecords = customRecords + + return a, nil +} + +func serializeHop(w io.Writer, h *route.Hop) error { + if err := WriteElements(w, + h.PubKeyBytes[:], + h.ChannelID, + h.OutgoingTimeLock, + h.AmtToForward, + ); err != nil { + return err + } + + if err := binary.Write(w, byteOrder, h.LegacyPayload); err != nil { + return err + } + + // For legacy payloads, we don't need to write any TLV records, so + // we'll write a zero indicating the our serialized TLV map has no + // records. + if h.LegacyPayload { + return WriteElements(w, uint32(0)) + } + + // Gather all non-primitive TLV records so that they can be serialized + // as a single blob. + // + // TODO(conner): add migration to unify all fields in a single TLV + // blobs. The split approach will cause headaches down the road as more + // fields are added, which we can avoid by having a single TLV stream + // for all payload fields. + var records []tlv.Record + if h.MPP != nil { + records = append(records, h.MPP.Record()) + } + + // Add blinding point and encrypted data if present. + if h.EncryptedData != nil { + records = append(records, record.NewEncryptedDataRecord( + &h.EncryptedData, + )) + } + + if h.BlindingPoint != nil { + records = append(records, record.NewBlindingPointRecord( + &h.BlindingPoint, + )) + } + + if h.AMP != nil { + records = append(records, h.AMP.Record()) + } + + if h.Metadata != nil { + records = append(records, record.NewMetadataRecord(&h.Metadata)) + } + + if h.TotalAmtMsat != 0 { + totalMsatInt := uint64(h.TotalAmtMsat) + records = append( + records, record.NewTotalAmtMsatBlinded(&totalMsatInt), + ) + } + + // Final sanity check to absolutely rule out custom records that are not + // custom and write into the standard range. + if err := h.CustomRecords.Validate(); err != nil { + return err + } + + // Convert custom records to tlv and add to the record list. + // MapToRecords sorts the list, so adding it here will keep the list + // canonical. + tlvRecords := tlv.MapToRecords(h.CustomRecords) + records = append(records, tlvRecords...) + + // Otherwise, we'll transform our slice of records into a map of the + // raw bytes, then serialize them in-line with a length (number of + // elements) prefix. + mapRecords, err := tlv.RecordsToMap(records) + if err != nil { + return err + } + + numRecords := uint32(len(mapRecords)) + if err := WriteElements(w, numRecords); err != nil { + return err + } + + for recordType, rawBytes := range mapRecords { + if err := WriteElements(w, recordType); err != nil { + return err + } + + if err := wire.WriteVarBytes(w, 0, rawBytes); err != nil { + return err + } + } + + return nil +} + +// maxOnionPayloadSize is the largest Sphinx payload possible, so we don't need +// to read/write a TLV stream larger than this. +const maxOnionPayloadSize = 1300 + +func deserializeHop(r io.Reader) (*route.Hop, error) { + h := &route.Hop{} + + var pub []byte + if err := ReadElements(r, &pub); err != nil { + return nil, err + } + copy(h.PubKeyBytes[:], pub) + + if err := ReadElements(r, + &h.ChannelID, &h.OutgoingTimeLock, &h.AmtToForward, + ); err != nil { + return nil, err + } + + // TODO(roasbeef): change field to allow LegacyPayload false to be the + // legacy default? + err := binary.Read(r, byteOrder, &h.LegacyPayload) + if err != nil { + return nil, err + } + + var numElements uint32 + if err := ReadElements(r, &numElements); err != nil { + return nil, err + } + + // If there're no elements, then we can return early. + if numElements == 0 { + return h, nil + } + + tlvMap := make(map[uint64][]byte) + for i := uint32(0); i < numElements; i++ { + var tlvType uint64 + if err := ReadElements(r, &tlvType); err != nil { + return nil, err + } + + rawRecordBytes, err := wire.ReadVarBytes( + r, 0, maxOnionPayloadSize, "tlv", + ) + if err != nil { + return nil, err + } + + tlvMap[tlvType] = rawRecordBytes + } + + // If the MPP type is present, remove it from the generic TLV map and + // parse it back into a proper MPP struct. + // + // TODO(conner): add migration to unify all fields in a single TLV + // blobs. The split approach will cause headaches down the road as more + // fields are added, which we can avoid by having a single TLV stream + // for all payload fields. + mppType := uint64(record.MPPOnionType) + if mppBytes, ok := tlvMap[mppType]; ok { + delete(tlvMap, mppType) + + var ( + mpp = &record.MPP{} + mppRec = mpp.Record() + r = bytes.NewReader(mppBytes) + ) + err := mppRec.Decode(r, uint64(len(mppBytes))) + if err != nil { + return nil, err + } + h.MPP = mpp + } + + // If encrypted data or blinding key are present, remove them from + // the TLV map and parse into proper types. + encryptedDataType := uint64(record.EncryptedDataOnionType) + if data, ok := tlvMap[encryptedDataType]; ok { + delete(tlvMap, encryptedDataType) + h.EncryptedData = data + } + + blindingType := uint64(record.BlindingPointOnionType) + if blindingPoint, ok := tlvMap[blindingType]; ok { + delete(tlvMap, blindingType) + + h.BlindingPoint, err = btcec.ParsePubKey(blindingPoint) + if err != nil { + return nil, fmt.Errorf("invalid blinding point: %w", + err) + } + } + + ampType := uint64(record.AMPOnionType) + if ampBytes, ok := tlvMap[ampType]; ok { + delete(tlvMap, ampType) + + var ( + amp = &record.AMP{} + ampRec = amp.Record() + r = bytes.NewReader(ampBytes) + ) + err := ampRec.Decode(r, uint64(len(ampBytes))) + if err != nil { + return nil, err + } + h.AMP = amp + } + + // If the metadata type is present, remove it from the tlv map and + // populate directly on the hop. + metadataType := uint64(record.MetadataOnionType) + if metadata, ok := tlvMap[metadataType]; ok { + delete(tlvMap, metadataType) + + h.Metadata = metadata + } + + totalAmtMsatType := uint64(record.TotalAmtMsatBlindedType) + if totalAmtMsat, ok := tlvMap[totalAmtMsatType]; ok { + delete(tlvMap, totalAmtMsatType) + + var ( + totalAmtMsatInt uint64 + buf [8]byte + ) + if err := tlv.DTUint64( + bytes.NewReader(totalAmtMsat), + &totalAmtMsatInt, + &buf, + uint64(len(totalAmtMsat)), + ); err != nil { + return nil, err + } + + h.TotalAmtMsat = lnwire.MilliSatoshi(totalAmtMsatInt) + } + + h.CustomRecords = tlvMap + + return h, nil +} + +// SerializeRoute serializes a route. +func SerializeRoute(w io.Writer, r route.Route) error { + if err := WriteElements(w, + r.TotalTimeLock, r.TotalAmount, r.SourcePubKey[:], + ); err != nil { + return err + } + + if err := WriteElements(w, uint32(len(r.Hops))); err != nil { + return err + } + + for _, h := range r.Hops { + if err := serializeHop(w, h); err != nil { + return err + } + } + + // Any new/extra TLV data is encoded in serializeHTLCAttemptInfo! + + return nil +} + +// DeserializeRoute deserializes a route. +func DeserializeRoute(r io.Reader) (route.Route, error) { + rt := route.Route{} + if err := ReadElements(r, + &rt.TotalTimeLock, &rt.TotalAmount, + ); err != nil { + return rt, err + } + + var pub []byte + if err := ReadElements(r, &pub); err != nil { + return rt, err + } + copy(rt.SourcePubKey[:], pub) + + var numHops uint32 + if err := ReadElements(r, &numHops); err != nil { + return rt, err + } + + var hops []*route.Hop + for i := uint32(0); i < numHops; i++ { + hop, err := deserializeHop(r) + if err != nil { + return rt, err + } + hops = append(hops, hop) + } + rt.Hops = hops + + // Any new/extra TLV data is decoded in deserializeHTLCAttemptInfo! + + return rt, nil +} + +// serializeHTLCSettleInfo serializes the details of a settled htlc. +func serializeHTLCSettleInfo(w io.Writer, s *HTLCSettleInfo) error { + if _, err := w.Write(s.Preimage[:]); err != nil { + return err + } + + if err := serializeTime(w, s.SettleTime); err != nil { + return err + } + + return nil +} + +// deserializeHTLCSettleInfo deserializes the details of a settled htlc. +func deserializeHTLCSettleInfo(r io.Reader) (*HTLCSettleInfo, error) { + s := &HTLCSettleInfo{} + if _, err := io.ReadFull(r, s.Preimage[:]); err != nil { + return nil, err + } + + var err error + s.SettleTime, err = deserializeTime(r) + if err != nil { + return nil, err + } + + return s, nil +} + +// serializeHTLCFailInfo serializes the details of a failed htlc including the +// wire failure. +func serializeHTLCFailInfo(w io.Writer, f *HTLCFailInfo) error { + if err := serializeTime(w, f.FailTime); err != nil { + return err + } + + // Write failure. If there is no failure message, write an empty + // byte slice. + var messageBytes bytes.Buffer + if f.Message != nil { + err := lnwire.EncodeFailureMessage(&messageBytes, f.Message, 0) + if err != nil { + return err + } + } + if err := wire.WriteVarBytes(w, 0, messageBytes.Bytes()); err != nil { + return err + } + + return WriteElements(w, byte(f.Reason), f.FailureSourceIndex) +} + +// deserializeHTLCFailInfo deserializes the details of a failed htlc including +// the wire failure. +func deserializeHTLCFailInfo(r io.Reader) (*HTLCFailInfo, error) { + f := &HTLCFailInfo{} + var err error + f.FailTime, err = deserializeTime(r) + if err != nil { + return nil, err + } + + // Read failure. + failureBytes, err := wire.ReadVarBytes( + r, 0, math.MaxUint16, "failure", + ) + if err != nil { + return nil, err + } + if len(failureBytes) > 0 { + f.Message, err = lnwire.DecodeFailureMessage( + bytes.NewReader(failureBytes), 0, + ) + if err != nil && + !errors.Is(err, lnwire.ErrParsingExtraTLVBytes) { + + return nil, err + } + + // In case we have an invalid TLV stream regarding the extra + // tlv data we still continue with the decoding of the + // HTLCFailInfo. + if errors.Is(err, lnwire.ErrParsingExtraTLVBytes) { + log.Warnf("Failed to decode extra TLV bytes for "+ + "failure message: %v", err) + } + } + + var reason byte + err = ReadElements(r, &reason, &f.FailureSourceIndex) + if err != nil { + return nil, err + } + f.Reason = HTLCFailReason(reason) + + return f, nil +} diff --git a/payments/db/migration1/log.go b/payments/db/migration1/log.go new file mode 100644 index 00000000000..52f1f750589 --- /dev/null +++ b/payments/db/migration1/log.go @@ -0,0 +1,32 @@ +package migration1 + +import ( + "github.com/btcsuite/btclog/v2" + "github.com/lightningnetwork/lnd/build" +) + +// log is a logger that is initialized with no output filters. This +// means the package will not perform any logging by default until the caller +// requests it. +var log btclog.Logger + +// Subsystem defines the logging identifier for this subsystem. +const Subsystem = "PYDB" + +// The default amount of logging is none. +func init() { + UseLogger(build.NewSubLogger(Subsystem, nil)) +} + +// DisableLog disables all library log output. Logging output is disabled +// by default until UseLogger is called. +func DisableLog() { + UseLogger(btclog.Disabled) +} + +// UseLogger uses a specified Logger to output package logging info. +// This should be used in preference to SetLogWriter if the caller is also +// using btclog. +func UseLogger(logger btclog.Logger) { + log = logger +} diff --git a/payments/db/migration1/migration_external_test.go b/payments/db/migration1/migration_external_test.go new file mode 100644 index 00000000000..76829c7b564 --- /dev/null +++ b/payments/db/migration1/migration_external_test.go @@ -0,0 +1,180 @@ +//go:build test_db_postgres || test_db_sqlite + +package migration1 + +import ( + "context" + "os" + "path" + "strings" + "testing" + "time" + + "github.com/btcsuite/btclog/v2" + "github.com/lightningnetwork/lnd/kvdb" + "github.com/lightningnetwork/lnd/kvdb/postgres" + "github.com/lightningnetwork/lnd/kvdb/sqlbase" + "github.com/lightningnetwork/lnd/kvdb/sqlite" + "github.com/lightningnetwork/lnd/sqldb" + "github.com/stretchr/testify/require" +) + +// TestMigrationWithExternalDB tests the migration of the payment store from a +// bolt backed channel.db or a kvdb channel.sqlite to a SQL database. Note that +// this test does not attempt to be a complete migration test for all payment +// store types but rather is added as a tool for developers and users to debug +// payment migration issues with an actual channel.db/channel.sqlite file. +// +// NOTE: To use this test, place either of those files in the +// payments/db/migration1/testdata directory, uncomment the "Skipf" line, and +// set the "fileName" variable to the name of the channel database file you +// want to use for the migration test. +func TestMigrationWithExternalDB(t *testing.T) { + ctx := context.Background() + + // NOTE: comment this line out to run the test. + t.Skipf("skipping test meant for local debugging only") + + // NOTE: set this to the name of the channel database file you want + // to use for the migration test. This may be either a bbolt ".db" file + // or a SQLite ".sqlite" file. If you want to migrate from a + // bbolt channel.db file, set this to "channel.db". + const fileName = "backup_channeldb.sqlite" + + // NOTE: if set, this test will prefer migrating from a Postgres-backed + // kvdb source instead of a local file. Leave empty to use fileName. + const postgresKVDSN = "" + const postgresKVPfx = "channeldb" + const logSequenceOrder = false + + // Determine if we are using a SQLite file or a Bolt DB file. + isSqlite := strings.HasSuffix(fileName, ".sqlite") + + // Set up logging for the test. + logger := btclog.NewSLogger(btclog.NewDefaultHandler(os.Stdout)) + UseLogger(logger) + + // migrate runs the migration from the kvdb store to the SQL store. + migrate := func(t *testing.T, kvBackend kvdb.Backend) { + sqlStore := setupTestSQLDB(t) + + // Run migration in a transaction + err := sqlStore.db.ExecTx( + ctx, sqldb.WriteTxOpt(), func(tx SQLQueries) error { + return MigratePaymentsKVToSQL( + ctx, kvBackend, tx, &SQLStoreConfig{ + QueryCfg: sqlStore.cfg.QueryCfg, + }, + ) + }, sqldb.NoOpReset, + ) + require.NoError(t, err) + + _ = logSequenceOrder + } + + connectPostgres := func(t *testing.T, dsn, prefix string) kvdb.Backend { + dsn = strings.TrimSpace(dsn) + if dsn == "" { + t.Fatalf("missing postgres kvdb dsn") + } + + prefix = strings.TrimSpace(prefix) + if prefix == "" { + prefix = "channeldb" + } + + const ( + timeout = 10 * time.Second + maxConns = 5 + ) + sqlbase.Init(maxConns) + + dbCfg := &postgres.Config{ + Dsn: dsn, + Timeout: timeout, + MaxConnections: maxConns, + } + + kvStore, err := kvdb.Open( + kvdb.PostgresBackendName, ctx, dbCfg, prefix, + ) + require.NoError(t, err) + + return kvStore + } + + connectPostgresKV := func(t *testing.T) kvdb.Backend { + return connectPostgres(t, postgresKVDSN, postgresKVPfx) + } + + connectBBolt := func(t *testing.T, dbPath string) kvdb.Backend { + cfg := &kvdb.BoltBackendConfig{ + DBPath: dbPath, + DBFileName: fileName, + NoFreelistSync: true, + AutoCompact: false, + AutoCompactMinAge: kvdb.DefaultBoltAutoCompactMinAge, + DBTimeout: kvdb.DefaultDBTimeout, + } + + kvStore, err := kvdb.GetBoltBackend(cfg) + require.NoError(t, err) + + return kvStore + } + + connectSQLite := func(t *testing.T, dbPath string) kvdb.Backend { + const ( + timeout = 10 * time.Second + maxConns = 5 + ) + sqlbase.Init(maxConns) + + cfg := &sqlite.Config{ + Timeout: timeout, + BusyTimeout: timeout, + MaxConnections: maxConns, + } + + kvStore, err := kvdb.Open( + kvdb.SqliteBackendName, ctx, cfg, + dbPath, fileName, + // NOTE: we use the raw string here else we get an + // import cycle if we try to import lncfg.NSChannelDB. + "channeldb", + ) + require.NoError(t, err) + + return kvStore + } + + tests := []struct { + name string + dbPath string + }{ + { + name: "testdata", + dbPath: "testdata", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if postgresKVDSN != "" { + migrate(t, connectPostgresKV(t)) + return + } + + chanDBPath := path.Join(test.dbPath, fileName) + t.Logf("Connecting to channel DB at: %s", chanDBPath) + + connectDB := connectBBolt + if isSqlite { + connectDB = connectSQLite + } + + migrate(t, connectDB(t, test.dbPath)) + }) + } +} diff --git a/payments/db/migration1/migration_validation.go b/payments/db/migration1/migration_validation.go new file mode 100644 index 00000000000..0757e0c75d8 --- /dev/null +++ b/payments/db/migration1/migration_validation.go @@ -0,0 +1,465 @@ +package migration1 + +import ( + "bytes" + "context" + "database/sql" + "fmt" + "reflect" + "sort" + "time" + + "github.com/davecgh/go-spew/spew" + "github.com/lightningnetwork/lnd/kvdb" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/payments/db/migration1/sqlc" + "github.com/pmezard/go-difflib/difflib" +) + +type migratedPaymentRef struct { + Hash lntypes.Hash + PaymentID int64 +} + +// validateMigratedPaymentBatch performs a deep validation pass by comparing +// KV payments with their SQL counterparts for a batch of payments. +func validateMigratedPaymentBatch(ctx context.Context, + kvBackend kvdb.Backend, sqlDB SQLQueries, + cfg *SQLStoreConfig, batch []migratedPaymentRef) error { + + if len(batch) == 0 { + return nil + } + + if cfg == nil || cfg.QueryCfg == nil { + return fmt.Errorf("missing SQL store config for validation") + } + + paymentIDs := make([]int64, 0, len(batch)) + for _, item := range batch { + paymentIDs = append(paymentIDs, item.PaymentID) + } + + rows, err := sqlDB.FetchPaymentsByIDs(ctx, paymentIDs) + if err != nil { + return fmt.Errorf("fetch SQL payments: %w", err) + } + if len(rows) != len(paymentIDs) { + return fmt.Errorf("SQL payment batch mismatch: got=%d want=%d", + len(rows), len(paymentIDs)) + } + + batchData, err := batchLoadPaymentDetailsData( + ctx, cfg.QueryCfg, sqlDB, paymentIDs, + ) + if err != nil { + return fmt.Errorf("load payment batch: %w", err) + } + + err = kvBackend.View(func(kvTx kvdb.RTx) error { + paymentsBucket := kvTx.ReadBucket(paymentsRootBucket) + if paymentsBucket == nil { + return fmt.Errorf("no payments bucket") + } + + for _, row := range rows { + payment := row.GetPayment() + hash := payment.PaymentIdentifier + var paymentHash lntypes.Hash + copy(paymentHash[:], hash) + + paymentBucket := paymentsBucket.NestedReadBucket(hash) + if paymentBucket == nil { + return fmt.Errorf("missing payment bucket %x", + hash[:8]) + } + + kvPayment, err := fetchPayment(paymentBucket) + if err != nil { + return fmt.Errorf("fetch KV payment %x: %w", + hash[:8], err) + } + + sqlPayment, err := buildPaymentFromBatchData( + row, batchData, + ) + if err != nil { + return fmt.Errorf("build SQL payment %x: %w", + hash[:8], err) + } + + normalizePaymentForCompare(kvPayment) + normalizePaymentForCompare(sqlPayment) + + if !reflect.DeepEqual(kvPayment, sqlPayment) { + // make sure we properly print the diff between + // the two payments if they are not equal. + dumpCfg := spew.ConfigState{ + DisablePointerAddresses: true, + DisableCapacities: true, + DisableMethods: true, + SortKeys: true, + } + diff := difflib.UnifiedDiff{ + A: difflib.SplitLines( + dumpCfg.Sdump(kvPayment), + ), + B: difflib.SplitLines( + dumpCfg.Sdump(sqlPayment), + ), + FromFile: "kv", + ToFile: "sql", + Context: 3, + } + diffText, _ := difflib.GetUnifiedDiffString( + diff, + ) + + return fmt.Errorf("payment mismatch %x\n%s", + hash[:8], diffText) + } + + err = compareDuplicatePayments( + ctx, paymentBucket, sqlDB, payment.ID, + paymentHash, + ) + if err != nil { + return err + } + } + + return nil + }, func() {}) + if err != nil { + return err + } + + return nil +} + +// normalizePaymentForCompare normalizes fields that are expected to differ +// between KV and SQL representations before deep comparison. +func normalizePaymentForCompare(payment *MPPayment) { + if payment == nil { + return + } + + // SequenceNum will not be equal because the kv db can have already + // payments deleted during its lifetime. + payment.SequenceNum = 0 + + trunc := func(t time.Time) time.Time { + if t.IsZero() { + return t + } + + return time.Unix(0, t.UnixNano()). + In(time.Local). + Truncate(time.Microsecond) + } + + // Normalize PaymentCreationInfo fields. + if payment.Info != nil { + payment.Info.CreationTime = trunc( + payment.Info.CreationTime, + ) + if len(payment.Info.PaymentRequest) == 0 { + payment.Info.PaymentRequest = nil + } + if len(payment.Info.FirstHopCustomRecords) == 0 { + payment.Info.FirstHopCustomRecords = nil + } + } + + // Normalize HTLCAttemptInfo list to empty if it is nil. + if len(payment.HTLCs) == 0 { + payment.HTLCs = []HTLCAttempt{} + } + + // Normalize HTLC attempt ordering; SQL/KV may return attempts + // in different orders. + sort.SliceStable(payment.HTLCs, func(i, j int) bool { + if payment.HTLCs[i].AttemptID == payment.HTLCs[j].AttemptID { + return payment.HTLCs[i].AttemptTime.Before( + payment.HTLCs[j].AttemptTime, + ) + } + + return payment.HTLCs[i].AttemptID < payment.HTLCs[j].AttemptID + }) + + // Normalize HTLCAttemptInfo fields. + for i := range payment.HTLCs { + htlc := &payment.HTLCs[i] + + htlc.AttemptTime = trunc(htlc.AttemptTime) + if htlc.Settle != nil { + htlc.Settle.SettleTime = trunc( + htlc.Settle.SettleTime, + ) + } + if htlc.Failure != nil { + htlc.Failure.FailTime = trunc( + htlc.Failure.FailTime, + ) + } + + // Clear cached fields not persisted in storage. + htlc.onionBlob = [1366]byte{} + htlc.circuit = nil + htlc.cachedSessionKey = nil + + if len(htlc.Route.FirstHopWireCustomRecords) == 0 { + htlc.Route.FirstHopWireCustomRecords = nil + } + + for j := range htlc.Route.Hops { + if len(htlc.Route.Hops[j].CustomRecords) == 0 { + htlc.Route.Hops[j].CustomRecords = nil + } + } + } +} + +type duplicateRecord struct { + PaymentIdentifier []byte + AmountMsat int64 + CreatedAt time.Time + FailReason sql.NullInt32 + SettlePreimage []byte + SettleTime sql.NullTime +} + +// compareDuplicatePayments validates migrated duplicate rows against KV data. +func compareDuplicatePayments(ctx context.Context, paymentBucket kvdb.RBucket, + sqlDB SQLQueries, paymentID int64, hash lntypes.Hash) error { + + kvDuplicates, err := fetchDuplicateRecords(paymentBucket) + if err != nil { + return fmt.Errorf("fetch KV duplicates %x: %w", + hash[:8], err) + } + + sqlDuplicates, err := sqlDB.FetchPaymentDuplicates(ctx, paymentID) + if err != nil { + return fmt.Errorf("fetch SQL duplicates %x: %w", + hash[:8], err) + } + + if len(kvDuplicates) != len(sqlDuplicates) { + return fmt.Errorf("duplicate count mismatch %x: kv=%d "+ + "sql=%d", hash[:8], len(kvDuplicates), + len(sqlDuplicates)) + } + + kvNormalized := normalizeDuplicateRecords(kvDuplicates) + sqlNormalized := normalizeDuplicateRecords( + convertSQLDuplicates(sqlDuplicates), + ) + + sortDuplicates(kvNormalized) + sortDuplicates(sqlNormalized) + + if !reflect.DeepEqual(kvNormalized, sqlNormalized) { + dumpCfg := spew.ConfigState{ + DisablePointerAddresses: true, + DisableCapacities: true, + DisableMethods: true, + SortKeys: true, + } + diff := difflib.UnifiedDiff{ + A: difflib.SplitLines( + dumpCfg.Sdump(kvNormalized), + ), + B: difflib.SplitLines( + dumpCfg.Sdump(sqlNormalized), + ), + FromFile: "kv", + ToFile: "sql", + Context: 3, + } + diffText, _ := difflib.GetUnifiedDiffString(diff) + + return fmt.Errorf("duplicate mismatch %x\n%s", + hash[:8], diffText) + } + + return nil +} + +// fetchDuplicateRecords reads duplicate payment records from the KV bucket. +func fetchDuplicateRecords(paymentBucket kvdb.RBucket) ([]duplicateRecord, + error) { + + dupBucket := paymentBucket.NestedReadBucket(duplicatePaymentsBucket) + if dupBucket == nil { + return nil, nil + } + + var duplicates []duplicateRecord + err := dupBucket.ForEach(func(seqBytes, _ []byte) error { + if len(seqBytes) != 8 { + return nil + } + + subBucket := dupBucket.NestedReadBucket(seqBytes) + if subBucket == nil { + return nil + } + + creationData := subBucket.Get(duplicatePaymentCreationInfoKey) + if creationData == nil { + return fmt.Errorf("missing duplicate creation info") + } + + creationInfo, err := deserializeDuplicatePaymentCreationInfo( + bytes.NewReader(creationData), + ) + if err != nil { + return fmt.Errorf("deserialize duplicate creation "+ + "info: %w", err) + } + + settleData := subBucket.Get(duplicatePaymentSettleInfoKey) + failReasonData := subBucket.Get(duplicatePaymentFailInfoKey) + + if settleData != nil && len(failReasonData) > 0 { + return fmt.Errorf("duplicate has both settle and " + + "fail info") + } + + var ( + failReason sql.NullInt32 + settlePreimage []byte + settleTime sql.NullTime + ) + + switch { + case settleData != nil: + settlePreimage, settleTime, err = + parseDuplicateSettleData(settleData) + if err != nil { + return err + } + case len(failReasonData) > 0: + failReason = sql.NullInt32{ + Int32: int32(failReasonData[0]), + Valid: true, + } + default: + // If the duplicate has no settle or fail info, it is + // considered failed. Every duplicate payment must have + // either a settle or fail info in the sql database. So + // we set the fail reason to error to mimic the behavior + // for the kv store. + failReason = sql.NullInt32{ + Int32: int32(FailureReasonError), + Valid: true, + } + } + + duplicates = append(duplicates, duplicateRecord{ + PaymentIdentifier: creationInfo.PaymentIdentifier[:], + AmountMsat: int64(creationInfo.Value), + CreatedAt: normalizeTimeForSQL( + creationInfo.CreationTime, + ), + FailReason: failReason, + SettlePreimage: settlePreimage, + SettleTime: settleTime, + }) + + return nil + }) + if err != nil { + return nil, err + } + + return duplicates, nil +} + +// convertSQLDuplicates maps SQL duplicate rows into comparable records. +func convertSQLDuplicates(rows []sqlc.PaymentDuplicate) []duplicateRecord { + records := make([]duplicateRecord, 0, len(rows)) + for _, row := range rows { + records = append(records, duplicateRecord{ + PaymentIdentifier: row.PaymentIdentifier, + AmountMsat: row.AmountMsat, + CreatedAt: row.CreatedAt, + FailReason: row.FailReason, + SettlePreimage: row.SettlePreimage, + SettleTime: row.SettleTime, + }) + } + + return records +} + +// normalizeDuplicateRecords normalizes time precision and empty fields. +func normalizeDuplicateRecords(records []duplicateRecord) []duplicateRecord { + if len(records) == 0 { + return []duplicateRecord{} + } + + trunc := func(t time.Time) time.Time { + if t.IsZero() { + return t + } + + return t.In(time.Local).Truncate(time.Microsecond) + } + + for i := range records { + records[i].CreatedAt = trunc(records[i].CreatedAt) + if records[i].SettleTime.Valid { + records[i].SettleTime.Time = trunc( + records[i].SettleTime.Time, + ) + } + if len(records[i].SettlePreimage) == 0 { + records[i].SettlePreimage = nil + } + } + + return records +} + +// sortDuplicates orders records deterministically for deep comparison. +func sortDuplicates(records []duplicateRecord) { + sort.SliceStable(records, func(i, j int) bool { + ai := records[i] + aj := records[j] + + if ai.CreatedAt.Equal(aj.CreatedAt) { + if bytes.Equal( + ai.PaymentIdentifier, aj.PaymentIdentifier, + ) { + + return ai.AmountMsat < aj.AmountMsat + } + + return bytes.Compare( + ai.PaymentIdentifier, aj.PaymentIdentifier, + ) < 0 + } + + return ai.CreatedAt.Before(aj.CreatedAt) + }) +} + +// validatePaymentCounts compares the number of migrated payments with the SQL +// payment count to catch missing rows. +func validatePaymentCounts(ctx context.Context, sqlDB SQLQueries, + expectedCount int64) error { + + sqlCount, err := sqlDB.CountPayments(ctx) + if err != nil { + return fmt.Errorf("count SQL payments: %w", err) + } + if expectedCount != sqlCount { + return fmt.Errorf("payment count mismatch: kv=%d sql=%d", + expectedCount, sqlCount) + } + + return nil +} diff --git a/payments/db/migration1/options.go b/payments/db/migration1/options.go new file mode 100644 index 00000000000..382afb26c02 --- /dev/null +++ b/payments/db/migration1/options.go @@ -0,0 +1,26 @@ +package migration1 + +// StoreOptions holds parameters for the KVStore. +type StoreOptions struct { + // NoMigration allows to open the database in readonly mode + NoMigration bool +} + +// DefaultOptions returns a StoreOptions populated with default values. +func DefaultOptions() *StoreOptions { + return &StoreOptions{ + NoMigration: false, + } +} + +// OptionModifier is a function signature for modifying the default +// StoreOptions. +type OptionModifier func(*StoreOptions) + +// WithNoMigration allows the database to be opened in read only mode by +// disabling migrations. +func WithNoMigration(b bool) OptionModifier { + return func(o *StoreOptions) { + o.NoMigration = b + } +} diff --git a/payments/db/migration1/payment.go b/payments/db/migration1/payment.go new file mode 100644 index 00000000000..76d22154131 --- /dev/null +++ b/payments/db/migration1/payment.go @@ -0,0 +1,836 @@ +package migration1 + +import ( + "bytes" + "errors" + "fmt" + "time" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/davecgh/go-spew/spew" + sphinx "github.com/lightningnetwork/lightning-onion" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnutils" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" +) + +// FailureReason encodes the reason a payment ultimately failed. +type FailureReason byte + +const ( + // FailureReasonTimeout indicates that the payment did timeout before a + // successful payment attempt was made. + FailureReasonTimeout FailureReason = 0 + + // FailureReasonNoRoute indicates no successful route to the + // destination was found during path finding. + FailureReasonNoRoute FailureReason = 1 + + // FailureReasonError indicates that an unexpected error happened during + // payment. + FailureReasonError FailureReason = 2 + + // FailureReasonPaymentDetails indicates that either the hash is unknown + // or the final cltv delta or amount is incorrect. + FailureReasonPaymentDetails FailureReason = 3 + + // FailureReasonInsufficientBalance indicates that we didn't have enough + // balance to complete the payment. + FailureReasonInsufficientBalance FailureReason = 4 + + // FailureReasonCanceled indicates that the payment was canceled by the + // user. + FailureReasonCanceled FailureReason = 5 + + // TODO(joostjager): Add failure reasons for: + // LocalLiquidityInsufficient, RemoteCapacityInsufficient. +) + +// Error returns a human-readable error string for the FailureReason. +func (r FailureReason) Error() string { + return r.String() +} + +// String returns a human-readable FailureReason. +func (r FailureReason) String() string { + switch r { + case FailureReasonTimeout: + return "timeout" + case FailureReasonNoRoute: + return "no_route" + case FailureReasonError: + return "error" + case FailureReasonPaymentDetails: + return "incorrect_payment_details" + case FailureReasonInsufficientBalance: + return "insufficient_balance" + case FailureReasonCanceled: + return "canceled" + } + + return "unknown" +} + +// PaymentCreationInfo is the information necessary to have ready when +// initiating a payment, moving it into state InFlight. +type PaymentCreationInfo struct { + // PaymentIdentifier is the hash this payment is paying to in case of + // non-AMP payments, and the SetID for AMP payments. + PaymentIdentifier lntypes.Hash + + // Value is the amount we are paying. + Value lnwire.MilliSatoshi + + // CreationTime is the time when this payment was initiated. + CreationTime time.Time + + // PaymentRequest is the full payment request, if any. + PaymentRequest []byte + + // FirstHopCustomRecords are the TLV records that are to be sent to the + // first hop of this payment. These records will be transmitted via the + // wire message (UpdateAddHTLC) only and therefore do not affect the + // onion payload size. + FirstHopCustomRecords lnwire.CustomRecords +} + +// String returns a human-readable description of the payment creation info. +func (p *PaymentCreationInfo) String() string { + return fmt.Sprintf("payment_id=%v, amount=%v, created_at=%v", + p.PaymentIdentifier, p.Value, p.CreationTime) +} + +// HTLCAttemptInfo contains static information about a specific HTLC attempt +// for a payment. This information is used by the router to handle any errors +// coming back after an attempt is made, and to query the switch about the +// status of the attempt. +type HTLCAttemptInfo struct { + // AttemptID is the unique ID used for this attempt. + AttemptID uint64 + + // sessionKey is the raw bytes ephemeral key used for this attempt. + // These bytes are lazily read off disk to save ourselves the expensive + // EC operations used by btcec.PrivKeyFromBytes. + sessionKey [btcec.PrivKeyBytesLen]byte + + // cachedSessionKey is our fully deserialized sesionKey. This value + // may be nil if the attempt has just been read from disk and its + // session key has not been used yet. + cachedSessionKey *btcec.PrivateKey + + // Route is the route attempted to send the HTLC. + Route route.Route + + // AttemptTime is the time at which this HTLC was attempted. + AttemptTime time.Time + + // Hash is the hash used for this single HTLC attempt. For AMP payments + // this will differ across attempts, for non-AMP payments each attempt + // will use the same hash. This can be nil for older payment attempts, + // in which the payment's PaymentHash in the PaymentCreationInfo should + // be used. + Hash *lntypes.Hash + + // onionBlob is the cached value for onion blob created from the sphinx + // construction. + onionBlob [lnwire.OnionPacketSize]byte + + // circuit is the cached value for sphinx circuit. + circuit *sphinx.Circuit +} + +// NewHtlcAttempt creates a htlc attempt. +func NewHtlcAttempt(attemptID uint64, sessionKey *btcec.PrivateKey, + route route.Route, attemptTime time.Time, + hash *lntypes.Hash) (*HTLCAttempt, error) { + + var scratch [btcec.PrivKeyBytesLen]byte + copy(scratch[:], sessionKey.Serialize()) + + info := HTLCAttemptInfo{ + AttemptID: attemptID, + sessionKey: scratch, + cachedSessionKey: sessionKey, + Route: route, + AttemptTime: attemptTime, + Hash: hash, + } + + if err := info.attachOnionBlobAndCircuit(); err != nil { + return nil, err + } + + return &HTLCAttempt{HTLCAttemptInfo: info}, nil +} + +// SessionKey returns the ephemeral key used for a htlc attempt. This function +// performs expensive ec-ops to obtain the session key if it is not cached. +func (h *HTLCAttemptInfo) SessionKey() *btcec.PrivateKey { + if h.cachedSessionKey == nil { + h.cachedSessionKey, _ = btcec.PrivKeyFromBytes( + h.sessionKey[:], + ) + } + + return h.cachedSessionKey +} + +// setSessionKey sets the session key for the htlc attempt. +// +// NOTE: Only used for testing. +// +//nolint:unused +func (h *HTLCAttemptInfo) setSessionKey(sessionKey *btcec.PrivateKey) { + h.cachedSessionKey = sessionKey + + // Also set the session key as a raw bytes. + var scratch [btcec.PrivKeyBytesLen]byte + copy(scratch[:], sessionKey.Serialize()) + h.sessionKey = scratch +} + +// OnionBlob returns the onion blob created from the sphinx construction. +func (h *HTLCAttemptInfo) OnionBlob() ([lnwire.OnionPacketSize]byte, error) { + var zeroBytes [lnwire.OnionPacketSize]byte + if h.onionBlob == zeroBytes { + if err := h.attachOnionBlobAndCircuit(); err != nil { + return zeroBytes, err + } + } + + return h.onionBlob, nil +} + +// Circuit returns the sphinx circuit for this attempt. +func (h *HTLCAttemptInfo) Circuit() (*sphinx.Circuit, error) { + if h.circuit == nil { + if err := h.attachOnionBlobAndCircuit(); err != nil { + return nil, err + } + } + + return h.circuit, nil +} + +// attachOnionBlobAndCircuit creates a sphinx packet and caches the onion blob +// and circuit for this attempt. +func (h *HTLCAttemptInfo) attachOnionBlobAndCircuit() error { + onionBlob, circuit, err := generateSphinxPacket( + &h.Route, h.Hash[:], h.SessionKey(), + ) + if err != nil { + return err + } + + copy(h.onionBlob[:], onionBlob) + h.circuit = circuit + + return nil +} + +// HTLCAttempt contains information about a specific HTLC attempt for a given +// payment. It contains the HTLCAttemptInfo used to send the HTLC, as well +// as a timestamp and any known outcome of the attempt. +type HTLCAttempt struct { + HTLCAttemptInfo + + // Settle is the preimage of a successful payment. This serves as a + // proof of payment. It will only be non-nil for settled payments. + // + // NOTE: Can be nil if payment is not settled. + Settle *HTLCSettleInfo + + // Fail is a failure reason code indicating the reason the payment + // failed. It is only non-nil for failed payments. + // + // NOTE: Can be nil if payment is not failed. + Failure *HTLCFailInfo +} + +// HTLCSettleInfo encapsulates the information that augments an HTLCAttempt in +// the event that the HTLC is successful. +type HTLCSettleInfo struct { + // Preimage is the preimage of a successful HTLC. This serves as a proof + // of payment. + Preimage lntypes.Preimage + + // SettleTime is the time at which this HTLC was settled. + SettleTime time.Time +} + +// HTLCFailReason is the reason an htlc failed. +type HTLCFailReason byte + +const ( + // HTLCFailUnknown is recorded for htlcs that failed with an unknown + // reason. + HTLCFailUnknown HTLCFailReason = 0 + + // HTLCFailUnreadable is recorded for htlcs that had a failure message + // that couldn't be decrypted. + HTLCFailUnreadable HTLCFailReason = 1 + + // HTLCFailInternal is recorded for htlcs that failed because of an + // internal error. + HTLCFailInternal HTLCFailReason = 2 + + // HTLCFailMessage is recorded for htlcs that failed with a network + // failure message. + HTLCFailMessage HTLCFailReason = 3 +) + +// HTLCFailInfo encapsulates the information that augments an HTLCAttempt in the +// event that the HTLC fails. +type HTLCFailInfo struct { + // FailTime is the time at which this HTLC was failed. + FailTime time.Time + + // Message is the wire message that failed this HTLC. This field will be + // populated when the failure reason is HTLCFailMessage. + Message lnwire.FailureMessage + + // Reason is the failure reason for this HTLC. + Reason HTLCFailReason + + // The position in the path of the intermediate or final node that + // generated the failure message. Position zero is the sender node. This + // field will be populated when the failure reason is either + // HTLCFailMessage or HTLCFailUnknown. + FailureSourceIndex uint32 +} + +// MPPaymentState wraps a series of info needed for a given payment, which is +// used by both MPP and AMP. This is a memory representation of the payment's +// current state and is updated whenever the payment is read from disk. +type MPPaymentState struct { + // NumAttemptsInFlight specifies the number of HTLCs the payment is + // waiting results for. + NumAttemptsInFlight int + + // RemainingAmt specifies how much more money to be sent. + RemainingAmt lnwire.MilliSatoshi + + // FeesPaid specifies the total fees paid so far that can be used to + // calculate remaining fee budget. + FeesPaid lnwire.MilliSatoshi + + // HasSettledHTLC is true if at least one of the payment's HTLCs is + // settled. + HasSettledHTLC bool + + // PaymentFailed is true if the payment has been marked as failed with + // a reason. + PaymentFailed bool +} + +// MPPayment is a wrapper around a payment's PaymentCreationInfo and +// HTLCAttempts. All payments will have the PaymentCreationInfo set, any +// HTLCs made in attempts to be completed will populated in the HTLCs slice. +// Each populated HTLCAttempt represents an attempted HTLC, each of which may +// have the associated Settle or Fail struct populated if the HTLC is no longer +// in-flight. +type MPPayment struct { + // SequenceNum is a unique identifier used to sort the payments in + // order of creation. + SequenceNum uint64 + + // Info holds all static information about this payment, and is + // populated when the payment is initiated. + Info *PaymentCreationInfo + + // HTLCs holds the information about individual HTLCs that we send in + // order to make the payment. + HTLCs []HTLCAttempt + + // FailureReason is the failure reason code indicating the reason the + // payment failed. + // + // NOTE: Will only be set once the daemon has given up on the payment + // altogether. + FailureReason *FailureReason + + // Status is the current PaymentStatus of this payment. + Status PaymentStatus + + // State is the current state of the payment that holds a number of key + // insights and is used to determine what to do on each payment loop + // iteration. + State *MPPaymentState +} + +// Terminated returns a bool to specify whether the payment is in a terminal +// state. +func (m *MPPayment) Terminated() bool { + // If the payment is in terminal state, it cannot be updated. + return m.Status.updatable() != nil +} + +// TerminalInfo returns any HTLC settle info recorded. If no settle info is +// recorded, any payment level failure will be returned. If neither a settle +// nor a failure is recorded, both return values will be nil. +func (m *MPPayment) TerminalInfo() (*HTLCAttempt, *FailureReason) { + for _, h := range m.HTLCs { + if h.Settle != nil { + return &h, nil + } + } + + return nil, m.FailureReason +} + +// SentAmt returns the sum of sent amount and fees for HTLCs that are either +// settled or still in flight. +func (m *MPPayment) SentAmt() (lnwire.MilliSatoshi, lnwire.MilliSatoshi) { + var sent, fees lnwire.MilliSatoshi + for _, h := range m.HTLCs { + if h.Failure != nil { + continue + } + + // The attempt was not failed, meaning the amount was + // potentially sent to the receiver. + sent += h.Route.ReceiverAmt() + fees += h.Route.TotalFees() + } + + return sent, fees +} + +// InFlightHTLCs returns the HTLCs that are still in-flight, meaning they have +// not been settled or failed. +func (m *MPPayment) InFlightHTLCs() []HTLCAttempt { + var inflights []HTLCAttempt + for _, h := range m.HTLCs { + if h.Settle != nil || h.Failure != nil { + continue + } + + inflights = append(inflights, h) + } + + return inflights +} + +// GetAttempt returns the specified htlc attempt on the payment. +func (m *MPPayment) GetAttempt(id uint64) (*HTLCAttempt, error) { + // TODO(yy): iteration can be slow, make it into a tree or use BS. + for _, htlc := range m.HTLCs { + htlc := htlc + if htlc.AttemptID == id { + return &htlc, nil + } + } + + return nil, errors.New("htlc attempt not found on payment") +} + +// Registrable returns an error to specify whether adding more HTLCs to the +// payment with its current status is allowed. A payment can accept new HTLC +// registrations when it's newly created, or none of its HTLCs is in a terminal +// state. +func (m *MPPayment) Registrable() error { + // If updating the payment is not allowed, we can't register new HTLCs. + // Otherwise, the status must be either `StatusInitiated` or + // `StatusInFlight`. + if err := m.Status.updatable(); err != nil { + return err + } + + // Exit early if this is not inflight. + if m.Status != StatusInFlight { + return nil + } + + // There are still inflight HTLCs and we need to check whether there + // are settled HTLCs or the payment is failed. If we already have + // settled HTLCs, we won't allow adding more HTLCs. + if m.State.HasSettledHTLC { + return ErrPaymentPendingSettled + } + + // If the payment is already failed, we won't allow adding more HTLCs. + if m.State.PaymentFailed { + return ErrPaymentPendingFailed + } + + // Otherwise we can add more HTLCs. + return nil +} + +// setState creates and attaches a new MPPaymentState to the payment. It also +// updates the payment's status based on its current state. +func (m *MPPayment) setState() error { + // Fetch the total amount and fees that has already been sent in + // settled and still in-flight shards. + sentAmt, fees := m.SentAmt() + + // Sanity check we haven't sent a value larger than the payment amount. + totalAmt := m.Info.Value + if sentAmt > totalAmt { + return fmt.Errorf("%w: sent=%v, total=%v", + ErrSentExceedsTotal, sentAmt, totalAmt) + } + + // Get any terminal info for this payment. + settle, failure := m.TerminalInfo() + + // Now determine the payment's status. + status, err := decidePaymentStatus(m.HTLCs, m.FailureReason) + if err != nil { + return err + } + + // Update the payment state and status. + m.State = &MPPaymentState{ + NumAttemptsInFlight: len(m.InFlightHTLCs()), + RemainingAmt: totalAmt - sentAmt, + FeesPaid: fees, + HasSettledHTLC: settle != nil, + PaymentFailed: failure != nil, + } + m.Status = status + + return nil +} + +// SetState calls the internal method setState. This is a temporary method +// to be used by the tests in routing. Once the tests are updated to use mocks, +// this method can be removed. +// +// TODO(yy): delete. +func (m *MPPayment) SetState() error { + return m.setState() +} + +// NeedWaitAttempts decides whether we need to hold creating more HTLC attempts +// and wait for the results of the payment's inflight HTLCs. Return an error if +// the payment is in an unexpected state. +func (m *MPPayment) NeedWaitAttempts() (bool, error) { + // Check when the remainingAmt is not zero, which means we have more + // money to be sent. + if m.State.RemainingAmt != 0 { + switch m.Status { + // If the payment is newly created, no need to wait for HTLC + // results. + case StatusInitiated: + return false, nil + + // If we have inflight HTLCs, we'll check if we have terminal + // states to decide if we need to wait. + case StatusInFlight: + // We still have money to send, and one of the HTLCs is + // settled. We'd stop sending money and wait for all + // inflight HTLC attempts to finish. + if m.State.HasSettledHTLC { + log.Warnf("payment=%v has remaining amount "+ + "%v, yet at least one of its HTLCs is "+ + "settled", m.Info.PaymentIdentifier, + m.State.RemainingAmt) + + return true, nil + } + + // The payment has a failure reason though we still + // have money to send, we'd stop sending money and wait + // for all inflight HTLC attempts to finish. + if m.State.PaymentFailed { + return true, nil + } + + // Otherwise we don't need to wait for inflight HTLCs + // since we still have money to be sent. + return false, nil + + // We need to send more money, yet the payment is already + // succeeded. Return an error in this case as the receiver is + // violating the protocol. + case StatusSucceeded: + return false, fmt.Errorf("%w: parts of the payment "+ + "already succeeded but still have remaining "+ + "amount %v", ErrPaymentInternal, + m.State.RemainingAmt) + + // The payment is failed and we have no inflight HTLCs, no need + // to wait. + case StatusFailed: + return false, nil + + // Unknown payment status. + default: + return false, fmt.Errorf("%w: %s", + ErrUnknownPaymentStatus, m.Status) + } + } + + // Now we determine whether we need to wait when the remainingAmt is + // already zero. + switch m.Status { + // When the payment is newly created, yet the payment has no remaining + // amount, return an error. + case StatusInitiated: + return false, fmt.Errorf("%w: %v", + ErrPaymentInternal, m.Status) + + // If the payment is inflight, we must wait. + // + // NOTE: an edge case is when all HTLCs are failed while the payment is + // not failed we'd still be in this inflight state. However, since the + // remainingAmt is zero here, it means we cannot be in that state as + // otherwise the remainingAmt would not be zero. + case StatusInFlight: + return true, nil + + // If the payment is already succeeded, no need to wait. + case StatusSucceeded: + return false, nil + + // If the payment is already failed, yet the remaining amount is zero, + // return an error as this indicates an error state. We will only each + // this status when there are no inflight HTLCs and the payment is + // marked as failed with a reason, which means the remainingAmt must + // not be zero because our sentAmt is zero. + case StatusFailed: + return false, fmt.Errorf("%w: %v", + ErrPaymentInternal, m.Status) + + // Unknown payment status. + default: + return false, fmt.Errorf("%w: %s", + ErrUnknownPaymentStatus, m.Status) + } +} + +// GetState returns the internal state of the payment. +func (m *MPPayment) GetState() *MPPaymentState { + return m.State +} + +// GetStatus returns the current status of the payment. +func (m *MPPayment) GetStatus() PaymentStatus { + return m.Status +} + +// GetHTLCs returns all the HTLCs for this payment. +func (m *MPPayment) GetHTLCs() []HTLCAttempt { + return m.HTLCs +} + +// AllowMoreAttempts is used to decide whether we can safely attempt more HTLCs +// for a given payment state. Return an error if the payment is in an +// unexpected state. +func (m *MPPayment) AllowMoreAttempts() (bool, error) { + // Now check whether the remainingAmt is zero or not. If we don't have + // any remainingAmt, no more HTLCs should be made. + if m.State.RemainingAmt == 0 { + // If the payment is newly created, yet we don't have any + // remainingAmt, return an error. + if m.Status == StatusInitiated { + return false, fmt.Errorf("%w: initiated payment has "+ + "zero remainingAmt", + ErrPaymentInternal) + } + + // Otherwise, exit early since all other statuses with zero + // remainingAmt indicate no more HTLCs can be made. + return false, nil + } + + // Otherwise, the remaining amount is not zero, we now decide whether + // to make more attempts based on the payment's current status. + // + // If at least one of the payment's attempts is settled, yet we haven't + // sent all the amount, it indicates something is wrong with the peer + // as the preimage is received. In this case, return an error state. + if m.Status == StatusSucceeded { + return false, fmt.Errorf("%w: payment already succeeded but "+ + "still have remaining amount %v", + ErrPaymentInternal, m.State.RemainingAmt) + } + + // Now check if we can register a new HTLC. + err := m.Registrable() + if err != nil { + log.Warnf("Payment(%v): cannot register HTLC attempt: %v, "+ + "current status: %s", m.Info.PaymentIdentifier, + err, m.Status) + + return false, nil + } + + // Now we know we can register new HTLCs. + return true, nil +} + +// generateSphinxPacket generates then encodes a sphinx packet which encodes +// the onion route specified by the passed layer 3 route. The blob returned +// from this function can immediately be included within an HTLC add packet to +// be sent to the first hop within the route. +func generateSphinxPacket(rt *route.Route, paymentHash []byte, + sessionKey *btcec.PrivateKey) ([]byte, *sphinx.Circuit, error) { + + // Now that we know we have an actual route, we'll map the route into a + // sphinx payment path which includes per-hop payloads for each hop + // that give each node within the route the necessary information + // (fees, CLTV value, etc.) to properly forward the payment. + sphinxPath, err := rt.ToSphinxPath() + if err != nil { + return nil, nil, err + } + + log.Tracef("Constructed per-hop payloads for payment_hash=%x: %v", + paymentHash, lnutils.NewLogClosure(func() string { + path := make( + []sphinx.OnionHop, sphinxPath.TrueRouteLength(), + ) + for i := range path { + hopCopy := sphinxPath[i] + path[i] = hopCopy + } + + return spew.Sdump(path) + }), + ) + + // Next generate the onion routing packet which allows us to perform + // privacy preserving source routing across the network. + sphinxPacket, err := sphinx.NewOnionPacket( + sphinxPath, sessionKey, paymentHash, + sphinx.DeterministicPacketFiller, + ) + if err != nil { + return nil, nil, err + } + + // Finally, encode Sphinx packet using its wire representation to be + // included within the HTLC add packet. + var onionBlob bytes.Buffer + if err := sphinxPacket.Encode(&onionBlob); err != nil { + return nil, nil, err + } + + log.Tracef("Generated sphinx packet: %v", + lnutils.NewLogClosure(func() string { + // We make a copy of the ephemeral key and unset the + // internal curve here in order to keep the logs from + // getting noisy. + key := *sphinxPacket.EphemeralKey + packetCopy := *sphinxPacket + packetCopy.EphemeralKey = &key + + return spew.Sdump(packetCopy) + }), + ) + + return onionBlob.Bytes(), &sphinx.Circuit{ + SessionKey: sessionKey, + PaymentPath: sphinxPath.NodeKeys(), + }, nil +} + +// verifyAttempt validates that a new HTLC attempt is compatible with the +// existing payment and its in-flight HTLCs. This function checks: +// 1. MPP (Multi-Path Payment) compatibility between attempts +// 2. Blinded payment consistency +// 3. Amount validation +// 4. Total payment amount limits +func verifyAttempt(payment *MPPayment, attempt *HTLCAttemptInfo) error { + // If the final hop has encrypted data, then we know this is a + // blinded payment. In blinded payments, MPP records are not set + // for split payments and the recipient is responsible for using + // a consistent PathID across the various encrypted data + // payloads that we received from them for this payment. All we + // need to check is that the total amount field for each HTLC + // in the split payment is correct. + isBlinded := len(attempt.Route.FinalHop().EncryptedData) != 0 + + // For blinded payments, the last hop must set the total amount. + if isBlinded { + if attempt.Route.FinalHop().TotalAmtMsat == 0 { + return ErrBlindedPaymentMissingTotalAmount + } + } + + // Make sure any existing shards match the new one with regards + // to MPP options. + mpp := attempt.Route.FinalHop().MPP + + // MPP records should not be set for attempts to blinded paths. + if isBlinded && mpp != nil { + return ErrMPPRecordInBlindedPayment + } + + for _, h := range payment.InFlightHTLCs() { + hMpp := h.Route.FinalHop().MPP + hBlinded := len(h.Route.FinalHop().EncryptedData) != 0 + + // If this is a blinded payment, then no existing HTLCs + // should have MPP records. + if isBlinded && hMpp != nil { + return ErrMPPRecordInBlindedPayment + } + + // If the payment is blinded (previous attempts used blinded + // paths) and the attempt is not, or vice versa, return an + // error. + if isBlinded != hBlinded { + return ErrMixedBlindedAndNonBlindedPayments + } + + // If this is a blinded payment, then we just need to + // check that the TotalAmtMsat field for this shard + // is equal to that of any other shard in the same + // payment. + if isBlinded { + if attempt.Route.FinalHop().TotalAmtMsat != + h.Route.FinalHop().TotalAmtMsat { + + return ErrBlindedPaymentTotalAmountMismatch + } + + continue + } + + switch { + // We tried to register a non-MPP attempt for a MPP + // payment. + case mpp == nil && hMpp != nil: + return ErrMPPayment + + // We tried to register a MPP shard for a non-MPP + // payment. + case mpp != nil && hMpp == nil: + return ErrNonMPPayment + + // Non-MPP payment, nothing more to validate. + case mpp == nil: + continue + } + + // Check that MPP options match. + if mpp.PaymentAddr() != hMpp.PaymentAddr() { + return ErrMPPPaymentAddrMismatch + } + + if mpp.TotalMsat() != hMpp.TotalMsat() { + return ErrMPPTotalAmountMismatch + } + } + + // If this is a non-MPP attempt, it must match the total amount + // exactly. Note that a blinded payment is considered an MPP + // attempt. + amt := attempt.Route.ReceiverAmt() + if !isBlinded && mpp == nil && amt != payment.Info.Value { + return ErrValueMismatch + } + + // Ensure we aren't sending more than the total payment amount. + sentAmt, _ := payment.SentAmt() + if sentAmt+amt > payment.Info.Value { + return fmt.Errorf("%w: attempted=%v, payment amount=%v", + ErrValueExceedsAmt, sentAmt+amt, payment.Info.Value) + } + + return nil +} diff --git a/payments/db/migration1/payment_status.go b/payments/db/migration1/payment_status.go new file mode 100644 index 00000000000..16c4b90fba2 --- /dev/null +++ b/payments/db/migration1/payment_status.go @@ -0,0 +1,257 @@ +package migration1 + +import ( + "fmt" +) + +// PaymentStatus represent current status of payment. +type PaymentStatus byte + +const ( + // NOTE: PaymentStatus = 0 was previously used for status unknown and + // is now deprecated. + + // StatusInitiated is the status where a payment has just been + // initiated. + StatusInitiated PaymentStatus = 1 + + // StatusInFlight is the status where a payment has been initiated, but + // a response has not been received. + StatusInFlight PaymentStatus = 2 + + // StatusSucceeded is the status where a payment has been initiated and + // the payment was completed successfully. + StatusSucceeded PaymentStatus = 3 + + // StatusFailed is the status where a payment has been initiated and a + // failure result has come back. + StatusFailed PaymentStatus = 4 +) + +// errPaymentStatusUnknown is returned when a payment has an unknown status. +var errPaymentStatusUnknown = fmt.Errorf("unknown payment status") + +// String returns readable representation of payment status. +func (ps PaymentStatus) String() string { + switch ps { + case StatusInitiated: + return "Initiated" + + case StatusInFlight: + return "In Flight" + + case StatusSucceeded: + return "Succeeded" + + case StatusFailed: + return "Failed" + + default: + return "Unknown" + } +} + +// initializable returns an error to specify whether initiating the payment +// with its current status is allowed. A payment can only be initialized if it +// hasn't been created yet or already failed. +func (ps PaymentStatus) initializable() error { + switch ps { + // The payment has been created already. We will disallow creating it + // again in case other goroutines have already been creating HTLCs for + // it. + case StatusInitiated: + return ErrPaymentExists + + // We already have an InFlight payment on the network. We will disallow + // any new payments. + case StatusInFlight: + return ErrPaymentInFlight + + // The payment has been attempted and is succeeded so we won't allow + // creating it again. + case StatusSucceeded: + return ErrAlreadyPaid + + // We allow retrying failed payments. + case StatusFailed: + return nil + + default: + return fmt.Errorf("%w: %v", ErrUnknownPaymentStatus, + ps) + } +} + +// removable returns an error to specify whether deleting the payment with its +// current status is allowed. A payment cannot be safely deleted if it has +// inflight HTLCs. +func (ps PaymentStatus) removable() error { + switch ps { + // The payment has been created but has no HTLCs and can be removed. + case StatusInitiated: + return nil + + // There are still inflight HTLCs and the payment needs to wait for the + // final outcomes. + case StatusInFlight: + return ErrPaymentInFlight + + // The payment has been attempted and is succeeded and is allowed to be + // removed. + case StatusSucceeded: + return nil + + // Failed payments are allowed to be removed. + case StatusFailed: + return nil + + default: + return fmt.Errorf("%w: %v", ErrUnknownPaymentStatus, + ps) + } +} + +// updatable returns an error to specify whether the payment's HTLCs can be +// updated. A payment can update its HTLCs when it has inflight HTLCs. +func (ps PaymentStatus) updatable() error { + switch ps { + // Newly created payments can be updated. + case StatusInitiated: + return nil + + // Inflight payments can be updated. + case StatusInFlight: + return nil + + // If the payment has a terminal condition, we won't allow any updates. + case StatusSucceeded: + return ErrPaymentAlreadySucceeded + + case StatusFailed: + return ErrPaymentAlreadyFailed + + default: + return fmt.Errorf("%w: %v", ErrUnknownPaymentStatus, + ps) + } +} + +// decidePaymentStatus uses the payment's DB state to determine a memory status +// that's used by the payment router to decide following actions. +// Together, we use four variables to determine the payment's status, +// - inflight: whether there are any pending HTLCs. +// - settled: whether any of the HTLCs has been settled. +// - htlc failed: whether any of the HTLCs has been failed. +// - payment failed: whether the payment has been marked as failed. +// +// Based on the above variables, we derive the status using the following +// table, +// | inflight | settled | htlc failed | payment failed | status | +// |:--------:|:-------:|:-----------:|:--------------:|:--------------------:| +// | true | true | true | true | StatusInFlight | +// | true | true | true | false | StatusInFlight | +// | true | true | false | true | StatusInFlight | +// | true | true | false | false | StatusInFlight | +// | true | false | true | true | StatusInFlight | +// | true | false | true | false | StatusInFlight | +// | true | false | false | true | StatusInFlight | +// | true | false | false | false | StatusInFlight | +// | false | true | true | true | StatusSucceeded | +// | false | true | true | false | StatusSucceeded | +// | false | true | false | true | StatusSucceeded | +// | false | true | false | false | StatusSucceeded | +// | false | false | true | true | StatusFailed | +// | false | false | true | false | StatusInFlight | +// | false | false | false | true | StatusFailed | +// | false | false | false | false | StatusInitiated | +// +// When `inflight`, `settled`, `htlc failed`, and `payment failed` are false, +// this indicates the payment is newly created and hasn't made any HTLCs yet. +// When `inflight` and `settled` are false, `htlc failed` is true yet `payment +// failed` is false, this indicates all the payment's HTLCs have occurred a +// temporarily failure and the payment is still in-flight. +func decidePaymentStatus(htlcs []HTLCAttempt, + reason *FailureReason) (PaymentStatus, error) { + + var ( + inflight bool + htlcSettled bool + htlcFailed bool + paymentFailed bool + ) + + // If we have a failure reason, the payment is failed. + if reason != nil { + paymentFailed = true + } + + // Go through all HTLCs for this payment, check whether we have any + // settled HTLC, and any still in-flight. + for _, h := range htlcs { + if h.Failure != nil { + htlcFailed = true + continue + } + + if h.Settle != nil { + htlcSettled = true + continue + } + + // If any of the HTLCs are not failed nor settled, we + // still have inflight HTLCs. + inflight = true + } + + // Use the DB state to determine the status of the payment. + switch { + // If we have inflight HTLCs, no matter we have settled or failed + // HTLCs, or the payment failed, we still consider it inflight so we + // inform upper systems to wait for the results. + case inflight: + return StatusInFlight, nil + + // If we have no in-flight HTLCs, and at least one of the HTLCs is + // settled, the payment succeeded. + // + // NOTE: when reaching this case, paymentFailed could be true, which + // means we have a conflicting state for this payment. We choose to + // mark the payment as succeeded because it's the receiver's + // responsibility to only settle the payment iff all HTLCs are + // received. + case htlcSettled: + return StatusSucceeded, nil + + // If we have no in-flight HTLCs, and the payment failure is set, the + // payment is considered failed. + // + // NOTE: when reaching this case, settled must be false. + case paymentFailed: + return StatusFailed, nil + + // If we have no in-flight HTLCs, yet the payment is NOT failed, it + // means all the HTLCs are failed. In this case we can attempt more + // HTLCs. + // + // NOTE: when reaching this case, both settled and paymentFailed must + // be false. + case htlcFailed: + return StatusInFlight, nil + + // If none of the HTLCs is either settled or failed, and we have no + // inflight HTLCs, this means the payment has no HTLCs created yet. + // + // NOTE: when reaching this case, both settled and paymentFailed must + // be false. + case !htlcFailed: + return StatusInitiated, nil + + // Otherwise an impossible state is reached. + // + // NOTE: we should never end up here. + default: + log.Error("Impossible payment state reached") + return 0, fmt.Errorf("%w: payment is corrupted", + errPaymentStatusUnknown) + } +} diff --git a/payments/db/migration1/query.go b/payments/db/migration1/query.go new file mode 100644 index 00000000000..1fab2fbd9b8 --- /dev/null +++ b/payments/db/migration1/query.go @@ -0,0 +1,75 @@ +package migration1 + +const ( + // DefaultMaxPayments is the default maximum number of payments returned + // in the payments query pagination. + DefaultMaxPayments = 100 +) + +// Query represents a query to the payments database starting or ending +// at a certain offset index. The number of retrieved records can be limited. +type Query struct { + // IndexOffset determines the starting point of the payments query and + // is always exclusive. In normal order, the query starts at the next + // higher (available) index compared to IndexOffset. In reversed order, + // the query ends at the next lower (available) index compared to the + // IndexOffset. In the case of a zero index_offset, the query will start + // with the oldest payment when paginating forwards, or will end with + // the most recent payment when paginating backwards. + IndexOffset uint64 + + // MaxPayments is the maximal number of payments returned in the + // payments query. + MaxPayments uint64 + + // Reversed gives a meaning to the IndexOffset. If reversed is set to + // true, the query will fetch payments with indices lower than the + // IndexOffset, otherwise, it will return payments with indices greater + // than the IndexOffset. + Reversed bool + + // If IncludeIncomplete is true, then return payments that have not yet + // fully completed. This means that pending payments, as well as failed + // payments will show up if this field is set to true. + IncludeIncomplete bool + + // CountTotal indicates that all payments currently present in the + // payment index (complete and incomplete) should be counted. + CountTotal bool + + // CreationDateStart, expressed in Unix seconds, if set, filters out + // all payments with a creation date greater than or equal to it. + CreationDateStart int64 + + // CreationDateEnd, expressed in Unix seconds, if set, filters out all + // payments with a creation date less than or equal to it. + CreationDateEnd int64 +} + +// Response contains the result of a query to the payments database. +// It includes the set of payments that match the query and integers which +// represent the index of the first and last item returned in the series of +// payments. These integers allow callers to resume their query in the event +// that the query's response exceeds the max number of returnable events. +type Response struct { + // Payments is the set of payments returned from the database for the + // Query. + Payments []*MPPayment + + // FirstIndexOffset is the index of the first element in the set of + // returned MPPayments. Callers can use this to resume their query + // in the event that the slice has too many events to fit into a single + // response. The offset can be used to continue reverse pagination. + FirstIndexOffset uint64 + + // LastIndexOffset is the index of the last element in the set of + // returned MPPayments. Callers can use this to resume their query + // in the event that the slice has too many events to fit into a single + // response. The offset can be used to continue forward pagination. + LastIndexOffset uint64 + + // TotalCount represents the total number of payments that are currently + // stored in the payment database. This will only be set if the + // CountTotal field in the query was set to true. + TotalCount uint64 +} diff --git a/payments/db/migration1/sql_converters.go b/payments/db/migration1/sql_converters.go new file mode 100644 index 00000000000..ebd4764d0b6 --- /dev/null +++ b/payments/db/migration1/sql_converters.go @@ -0,0 +1,275 @@ +package migration1 + +import ( + "bytes" + "fmt" + "strconv" + "time" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/payments/db/migration1/sqlc" + "github.com/lightningnetwork/lnd/record" + "github.com/lightningnetwork/lnd/routing/route" + "github.com/lightningnetwork/lnd/tlv" +) + +// dbPaymentToCreationInfo converts database payment data to the +// PaymentCreationInfo struct. +func dbPaymentToCreationInfo(paymentIdentifier []byte, amountMsat int64, + createdAt time.Time, intentPayload []byte, + firstHopCustomRecords lnwire.CustomRecords) *PaymentCreationInfo { + + // This is the payment hash for non-AMP payments and the SetID for AMP + // payments. + var identifier lntypes.Hash + copy(identifier[:], paymentIdentifier) + + return &PaymentCreationInfo{ + PaymentIdentifier: identifier, + Value: lnwire.MilliSatoshi(amountMsat), + // The creation time is stored in the database as UTC but here + // we convert it to local time. + CreationTime: createdAt.Local(), + PaymentRequest: intentPayload, + FirstHopCustomRecords: firstHopCustomRecords, + } +} + +// dbAttemptToHTLCAttempt converts a database HTLC attempt to an HTLCAttempt. +func dbAttemptToHTLCAttempt(dbAttempt sqlc.FetchHtlcAttemptsForPaymentsRow, + hops []sqlc.FetchHopsForAttemptsRow, + hopCustomRecords map[int64][]sqlc.PaymentHopCustomRecord, + routeCustomRecords []sqlc.PaymentAttemptFirstHopCustomRecord) ( + *HTLCAttempt, error) { + + // Convert route-level first hop custom records to CustomRecords map. + var firstHopWireCustomRecords lnwire.CustomRecords + if len(routeCustomRecords) > 0 { + firstHopWireCustomRecords = make(lnwire.CustomRecords) + for _, record := range routeCustomRecords { + firstHopWireCustomRecords[uint64(record.Key)] = + record.Value + } + } + + // Build the route from the database data. + route, err := dbDataToRoute( + hops, hopCustomRecords, dbAttempt.FirstHopAmountMsat, + dbAttempt.RouteTotalTimeLock, dbAttempt.RouteTotalAmount, + dbAttempt.RouteSourceKey, firstHopWireCustomRecords, + ) + if err != nil { + return nil, fmt.Errorf("failed to convert to route: %w", + err) + } + + hash, err := lntypes.MakeHash(dbAttempt.PaymentHash) + if err != nil { + return nil, fmt.Errorf("failed to parse payment "+ + "hash: %w", err) + } + + // Create the attempt info. + var sessionKey [32]byte + copy(sessionKey[:], dbAttempt.SessionKey) + + info := HTLCAttemptInfo{ + AttemptID: uint64(dbAttempt.AttemptIndex), + sessionKey: sessionKey, + Route: *route, + AttemptTime: dbAttempt.AttemptTime, + Hash: &hash, + } + + attempt := &HTLCAttempt{ + HTLCAttemptInfo: info, + } + + // If there's no resolution type, the attempt is still in-flight. + // Return early without processing settlement or failure info. + if !dbAttempt.ResolutionType.Valid { + return attempt, nil + } + + // Add settlement info if present. + if HTLCAttemptResolutionType(dbAttempt.ResolutionType.Int32) == + HTLCAttemptResolutionSettled { + + var preimage lntypes.Preimage + copy(preimage[:], dbAttempt.SettlePreimage) + + attempt.Settle = &HTLCSettleInfo{ + Preimage: preimage, + SettleTime: dbAttempt.ResolutionTime.Time, + } + } + + // Add failure info if present. + if HTLCAttemptResolutionType(dbAttempt.ResolutionType.Int32) == + HTLCAttemptResolutionFailed { + + failure := &HTLCFailInfo{ + FailTime: dbAttempt.ResolutionTime.Time, + } + + if dbAttempt.HtlcFailReason.Valid { + failure.Reason = HTLCFailReason( + dbAttempt.HtlcFailReason.Int32, + ) + } + + if dbAttempt.FailureSourceIndex.Valid { + failure.FailureSourceIndex = uint32( + dbAttempt.FailureSourceIndex.Int32, + ) + } + + // Decode the failure message if present. + if len(dbAttempt.FailureMsg) > 0 { + msg, err := lnwire.DecodeFailureMessage( + bytes.NewReader(dbAttempt.FailureMsg), 0, + ) + if err != nil { + return nil, fmt.Errorf("failed to decode "+ + "failure message: %w", err) + } + failure.Message = msg + } + + attempt.Failure = failure + } + + return attempt, nil +} + +// dbDataToRoute converts database route data to a route.Route. +func dbDataToRoute(hops []sqlc.FetchHopsForAttemptsRow, + hopCustomRecords map[int64][]sqlc.PaymentHopCustomRecord, + firstHopAmountMsat int64, totalTimeLock int32, totalAmount int64, + sourceKey []byte, firstHopWireCustomRecords lnwire.CustomRecords) ( + *route.Route, error) { + + if len(hops) == 0 { + return nil, fmt.Errorf("no hops provided") + } + + // Hops are already sorted by hop_index from the SQL query. + routeHops := make([]*route.Hop, len(hops)) + + for i, hop := range hops { + pubKey, err := route.NewVertexFromBytes(hop.PubKey) + if err != nil { + return nil, fmt.Errorf("failed to parse pub key: %w", + err) + } + + var channelID uint64 + if hop.Scid != "" { + // The SCID is stored as a string representation + // of the uint64. + var err error + channelID, err = strconv.ParseUint(hop.Scid, 10, 64) + if err != nil { + return nil, fmt.Errorf("failed to parse "+ + "scid: %w", err) + } + } + + routeHop := &route.Hop{ + PubKeyBytes: pubKey, + ChannelID: channelID, + OutgoingTimeLock: uint32(hop.OutgoingTimeLock), + AmtToForward: lnwire.MilliSatoshi(hop.AmtToForward), + } + + // Add MPP record if present. + if len(hop.MppPaymentAddr) > 0 { + var paymentAddr [32]byte + copy(paymentAddr[:], hop.MppPaymentAddr) + routeHop.MPP = record.NewMPP( + lnwire.MilliSatoshi(hop.MppTotalMsat.Int64), + paymentAddr, + ) + } + + // Add AMP record if present. + if len(hop.AmpRootShare) > 0 { + var rootShare [32]byte + copy(rootShare[:], hop.AmpRootShare) + var setID [32]byte + copy(setID[:], hop.AmpSetID) + + routeHop.AMP = record.NewAMP( + rootShare, setID, + uint32(hop.AmpChildIndex.Int32), + ) + } + + // Add blinding point if present (only for introduction node + // in blinded route). + if len(hop.BlindingPoint) > 0 { + pubKey, err := btcec.ParsePubKey(hop.BlindingPoint) + if err != nil { + return nil, fmt.Errorf("failed to parse "+ + "blinding point: %w", err) + } + routeHop.BlindingPoint = pubKey + } + + // Add encrypted data if present (for all blinded hops). + if len(hop.EncryptedData) > 0 { + routeHop.EncryptedData = hop.EncryptedData + } + + // Add total amount if present (only for final hop in blinded + // route). + if hop.BlindedPathTotalAmt.Valid { + routeHop.TotalAmtMsat = lnwire.MilliSatoshi( + hop.BlindedPathTotalAmt.Int64, + ) + } + + // Add hop-level custom records. + if records, ok := hopCustomRecords[hop.ID]; ok { + routeHop.CustomRecords = make( + record.CustomSet, + ) + for _, rec := range records { + routeHop.CustomRecords[uint64(rec.Key)] = + rec.Value + } + } + + // Add metadata if present. + if len(hop.MetaData) > 0 { + routeHop.Metadata = hop.MetaData + } + + routeHops[i] = routeHop + } + + // Parse the source node public key. + var sourceNode route.Vertex + copy(sourceNode[:], sourceKey) + + route := &route.Route{ + TotalTimeLock: uint32(totalTimeLock), + TotalAmount: lnwire.MilliSatoshi(totalAmount), + SourcePubKey: sourceNode, + Hops: routeHops, + FirstHopWireCustomRecords: firstHopWireCustomRecords, + } + + // Set the first hop amount if it is set. + if firstHopAmountMsat != 0 { + route.FirstHopAmount = tlv.NewRecordT[tlv.TlvType0]( + tlv.NewBigSizeT(lnwire.MilliSatoshi( + firstHopAmountMsat, + )), + ) + } + + return route, nil +} diff --git a/payments/db/migration1/sql_migration.go b/payments/db/migration1/sql_migration.go new file mode 100644 index 00000000000..ea940233ebd --- /dev/null +++ b/payments/db/migration1/sql_migration.go @@ -0,0 +1,761 @@ +package migration1 + +import ( + "bytes" + "context" + "database/sql" + "fmt" + "strconv" + "time" + + "github.com/lightningnetwork/lnd/kvdb" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/payments/db/migration1/sqlc" + "github.com/lightningnetwork/lnd/routing/route" +) + +// MigrationStats tracks migration progress. +type MigrationStats struct { + TotalPayments int64 + SuccessfulPayments int64 + FailedPayments int64 + InFlightPayments int64 + TotalAttempts int64 + SettledAttempts int64 + FailedAttempts int64 + InFlightAttempts int64 + TotalHops int64 + DuplicatePayments int64 + DuplicateEntries int64 + MigrationDuration time.Duration +} + +// MigratePaymentsKVToSQL migrates payments from KV to SQL in a single +// transaction and validates migrated data in batches. This function is called +// by the migration framework which provides the transaction. +func MigratePaymentsKVToSQL(ctx context.Context, kvBackend kvdb.Backend, + sqlDB SQLQueries, cfg *SQLStoreConfig) error { + + if cfg == nil || cfg.QueryCfg == nil { + return fmt.Errorf("missing SQL store config for validation") + } + + if cfg.QueryCfg.MaxBatchSize == 0 { + return fmt.Errorf("invalid max batch size for validation") + } + + stats := &MigrationStats{} + startTime := time.Now() + + log.Infof("Starting payment migration from KV to SQL...") + + lastReport := time.Now() + var validationBatch []migratedPaymentRef + var validatedPayments int64 + + // Open the KV backend in read-only mode. + err := kvBackend.View(func(kvTx kvdb.RTx) error { + // In case we start with an empty database, there are no + // payments to migrate. + paymentsBucket := kvTx.ReadBucket(paymentsRootBucket) + if paymentsBucket == nil { + log.Infof("No payments bucket found - database is " + + "empty") + + return nil + } + + // The index bucket maps sequence number -> payment hash. + indexes := kvTx.ReadBucket(paymentsIndexBucket) + if indexes == nil { + return fmt.Errorf("index bucket does not exist") + } + + // We iterate over all sequence numbers in the index bucket to + // make sure we have the correct order of payments. Otherwise, + // if we just loop over the payments bucket, we might get the + // payments not in the chronological order but rather the + // lexicographical order of the payment hashes. + return indexes.ForEach(func(seqKey, indexVal []byte) error { + // Progress reporting based on time + actual work done + shouldReport := time.Since(lastReport) > 5*time.Second + shouldReport = shouldReport || + stats.TotalPayments%100 == 0 + + if shouldReport { + elapsed := time.Since(startTime) + paymentRate := float64(stats.TotalPayments) / + elapsed.Seconds() + attemptRate := float64(stats.TotalAttempts) / + elapsed.Seconds() + + log.Infof("Progress: %d payments, %d "+ + "attempts, %d hops | Rate: %.1f "+ + "pmt/s, %.1f att/s | Elapsed: %v", + stats.TotalPayments, + stats.TotalAttempts, stats.TotalHops, + paymentRate, attemptRate, + elapsed.Round(time.Second), + ) + lastReport = time.Now() + } + + r := bytes.NewReader(indexVal) + paymentHash, err := deserializePaymentIndex(r) + if err != nil { + return err + } + + paymentBucket := paymentsBucket.NestedReadBucket( + paymentHash[:], + ) + if paymentBucket == nil { + log.Warnf("Missing bucket for payment %x", + paymentHash[:8]) + + return nil + } + + seqBytes := paymentBucket.Get(paymentSequenceKey) + if seqBytes == nil { + return ErrNoSequenceNumber + } + + // Skip duplicates; they are migrated into the + // payment_duplicates table when the primary payment is + // processed. + if !bytes.Equal(seqBytes, seqKey) { + return nil + } + + // Fetch the payment from the kv store. + payment, err := fetchPayment(paymentBucket) + if err != nil { + return fmt.Errorf("fetch payment %x: %w", + paymentHash[:8], err) + } + + // Migrate the payment to the SQL database. + paymentID, err := migratePayment( + ctx, payment, paymentHash, sqlDB, stats, + ) + if err != nil { + return fmt.Errorf("migrate payment %x: %w", + paymentHash[:8], err) + } + + // Check for duplicates. + dupBucket := paymentBucket.NestedReadBucket( + duplicatePaymentsBucket, + ) + if dupBucket != nil { + err = migrateDuplicatePayments( + ctx, dupBucket, paymentHash, + paymentID, sqlDB, stats, + ) + if err != nil { + return fmt.Errorf("migrate duplicates "+ + "%x: %w", paymentHash[:8], + err) + } + } + + // Add the payment to the validation batch. + validationBatch = append( + validationBatch, migratedPaymentRef{ + Hash: paymentHash, + PaymentID: paymentID, + }, + ) + if uint32(len(validationBatch)) >= + cfg.QueryCfg.MaxBatchSize { + + err := validateMigratedPaymentBatch( + ctx, kvBackend, sqlDB, + cfg, + validationBatch, + ) + if err != nil { + return err + } + + validatedPayments += int64( + len(validationBatch), + ) + log.Infof("Validated %d/%d payments", + validatedPayments, + stats.TotalPayments, + ) + + validationBatch = validationBatch[:0] + } + + return nil + }) + }, func() {}) + + if err != nil { + return fmt.Errorf("migrate payments: %w", err) + } + + // Validate any remaining payments in the batch. + if len(validationBatch) > 0 { + if err := validateMigratedPaymentBatch( + ctx, kvBackend, sqlDB, cfg, validationBatch, + ); err != nil { + return err + } + + validatedPayments += int64(len(validationBatch)) + log.Infof("Validated %d/%d payments", validatedPayments, + stats.TotalPayments) + } + + // Validate the total number of payments as an additional sanity check. + if err := validatePaymentCounts( + ctx, sqlDB, stats.TotalPayments, + ); err != nil { + return err + } + + stats.MigrationDuration = time.Since(startTime) + + printMigrationSummary(stats) + + return nil +} + +// normalizeTimeForSQL converts a timestamp into the representation we persist +// and compare against in SQL: +// - drops any monotonic clock reading (SQL can't store it), +// - forces UTC for deterministic comparisons across environments. +// +// A zero time is returned unchanged. +func normalizeTimeForSQL(t time.Time) time.Time { + if t.IsZero() { + return t + } + + return time.Unix(0, t.UnixNano()).UTC() +} + +// migratePayment migrates a single payment from KV to SQL. +func migratePayment(ctx context.Context, payment *MPPayment, hash lntypes.Hash, + sqlDB SQLQueries, stats *MigrationStats) (int64, error) { + + // Update migration stats based on payment status. + switch payment.Status { + case StatusSucceeded: + stats.SuccessfulPayments++ + + case StatusFailed: + stats.FailedPayments++ + + case StatusInFlight: + stats.InFlightPayments++ + } + + // Prepare fail reason for SQL insert. + var failReason sql.NullInt32 + if payment.FailureReason != nil { + failReason = sql.NullInt32{ + Int32: int32(*payment.FailureReason), + Valid: true, + } + } + + // Insert payment using migration query. + paymentID, err := sqlDB.InsertPaymentMig( + ctx, sqlc.InsertPaymentMigParams{ + AmountMsat: int64(payment.Info.Value), + CreatedAt: normalizeTimeForSQL( + payment.Info.CreationTime, + ), + PaymentIdentifier: hash[:], + FailReason: failReason, + }) + if err != nil { + return 0, fmt.Errorf("insert payment: %w", err) + } + + // Insert payment intent. + // + // Only insert a row if we have an actual intent payload. For legacy + // hash-only/keysend-style payments, the intent may be absent. + if len(payment.Info.PaymentRequest) > 0 { + _, err = sqlDB.InsertPaymentIntent( + ctx, sqlc.InsertPaymentIntentParams{ + PaymentID: paymentID, + IntentType: int16(PaymentIntentTypeBolt11), + IntentPayload: payment.Info.PaymentRequest, + }, + ) + if err != nil { + return 0, fmt.Errorf("insert intent: %w", err) + } + } + + // Insert first hop custom records (payment level). + for key, value := range payment.Info.FirstHopCustomRecords { + err = sqlDB.InsertPaymentFirstHopCustomRecord(ctx, + sqlc.InsertPaymentFirstHopCustomRecordParams{ + PaymentID: paymentID, + Key: int64(key), + Value: value, + }) + if err != nil { + return 0, fmt.Errorf("insert custom record: %w", err) + } + } + + // Migrate HTLC attempts. + for _, htlc := range payment.HTLCs { + err = migrateHTLCAttempt( + ctx, paymentID, hash, &htlc, sqlDB, stats, + ) + if err != nil { + return 0, fmt.Errorf("migrate attempt %d: %w", + htlc.AttemptID, err) + } + } + + stats.TotalPayments++ + + return paymentID, nil +} + +// migrateHTLCAttempt migrates a single HTLC attempt. +func migrateHTLCAttempt(ctx context.Context, paymentID int64, + parentPaymentHash lntypes.Hash, htlc *HTLCAttempt, + sqlDB SQLQueries, stats *MigrationStats) error { + + // Validate that we have a payment hash for the attempt. + // + // NOTE: We always require an attempt payment hash. A missing hash is an + // unrecoverable inconsistency, because for AMP the hash may differ per + // shard and for MPP/legacy payments the absence indicates corruption. + var paymentHash []byte + switch { + case htlc.Hash != nil: + paymentHash = (*htlc.Hash)[:] + + default: + return fmt.Errorf("HTLC attempt %d missing payment hash "+ + "(parent payment hash=%x)", htlc.AttemptID, + parentPaymentHash[:]) + } + + firstHopAmountMsat := int64(htlc.Route.FirstHopAmount.Val.Int()) + + // Get the session key bytes. + sessionKeyBytes := htlc.SessionKey().Serialize() + + // Insert HTLC attempt. + _, err := sqlDB.InsertHtlcAttempt(ctx, sqlc.InsertHtlcAttemptParams{ + PaymentID: paymentID, + AttemptIndex: int64(htlc.AttemptID), + SessionKey: sessionKeyBytes, + AttemptTime: normalizeTimeForSQL(htlc.AttemptTime), + PaymentHash: paymentHash, + FirstHopAmountMsat: firstHopAmountMsat, + RouteTotalTimeLock: int32(htlc.Route.TotalTimeLock), + RouteTotalAmount: int64(htlc.Route.TotalAmount), + RouteSourceKey: htlc.Route.SourcePubKey[:], + }) + if err != nil { + return fmt.Errorf("insert HTLC attempt: %w", err) + } + + // Insert the route-level first hop custom records. + for key, value := range htlc.Route.FirstHopWireCustomRecords { + err = sqlDB.InsertPaymentAttemptFirstHopCustomRecord( + ctx, + sqlc.InsertPaymentAttemptFirstHopCustomRecordParams{ + HtlcAttemptIndex: int64(htlc.AttemptID), + Key: int64(key), + Value: value, + }, + ) + if err != nil { + return fmt.Errorf("insert attempt first hop custom "+ + "record: %w", err) + } + } + + // Insert route hops. + for hopIndex := range htlc.Route.Hops { + hop := htlc.Route.Hops[hopIndex] + err = migrateRouteHop( + ctx, int64(htlc.AttemptID), hopIndex, hop, + sqlDB, stats, + ) + if err != nil { + return fmt.Errorf("migrate hop %d: %w", hopIndex, err) + } + } + + // Handle attempt resolution (settle or fail). + switch { + case htlc.Settle != nil: + // Settled + err = sqlDB.SettleAttempt(ctx, sqlc.SettleAttemptParams{ + AttemptIndex: int64(htlc.AttemptID), + ResolutionTime: normalizeTimeForSQL( + htlc.Settle.SettleTime, + ), + ResolutionType: int32(HTLCAttemptResolutionSettled), + SettlePreimage: htlc.Settle.Preimage[:], + }) + if err != nil { + return fmt.Errorf("settle attempt: %w", err) + } + + stats.SettledAttempts++ + + case htlc.Failure != nil: + var failureMsg bytes.Buffer + if htlc.Failure.Message != nil { + err := lnwire.EncodeFailureMessage( + &failureMsg, htlc.Failure.Message, 0, + ) + if err != nil { + return fmt.Errorf("failed to encode "+ + "failure message: %w", err) + } + } + + err = sqlDB.FailAttempt(ctx, sqlc.FailAttemptParams{ + AttemptIndex: int64(htlc.AttemptID), + ResolutionTime: normalizeTimeForSQL( + htlc.Failure.FailTime, + ), + ResolutionType: int32(HTLCAttemptResolutionFailed), + FailureSourceIndex: sql.NullInt32{ + Int32: int32(htlc.Failure.FailureSourceIndex), + Valid: true, + }, + HtlcFailReason: sql.NullInt32{ + Int32: int32(htlc.Failure.Reason), + Valid: true, + }, + FailureMsg: failureMsg.Bytes(), + }) + if err != nil { + return fmt.Errorf("fail attempt: %w", err) + } + + stats.FailedAttempts++ + + default: + // If the attempt is not settled or failed, it is in flight. + stats.InFlightAttempts++ + } + + stats.TotalAttempts++ + + return nil +} + +// migrateRouteHop migrates a single route hop. +func migrateRouteHop(ctx context.Context, + attemptID int64, hopIndex int, hop *route.Hop, sqlDB SQLQueries, + stats *MigrationStats) error { + + // Convert channel ID to string representation of uint64. + // The SCID is stored as a decimal string to match the converter + // expectations (sql_converters.go:173). + scidStr := strconv.FormatUint(hop.ChannelID, 10) + + // Insert route hop. + hopID, err := sqlDB.InsertRouteHop(ctx, sqlc.InsertRouteHopParams{ + HtlcAttemptIndex: attemptID, + HopIndex: int32(hopIndex), + PubKey: hop.PubKeyBytes[:], + Scid: scidStr, + OutgoingTimeLock: int32(hop.OutgoingTimeLock), + AmtToForward: int64(hop.AmtToForward), + MetaData: hop.Metadata, + }) + if err != nil { + return fmt.Errorf("insert hop: %w", err) + } + + // Check for blinded route data (route blinding). + if len(hop.EncryptedData) > 0 || hop.BlindingPoint != nil || + hop.TotalAmtMsat != 0 { + + var blindingPoint []byte + if hop.BlindingPoint != nil { + blindingPoint = hop.BlindingPoint.SerializeCompressed() + } + + var totalAmt sql.NullInt64 + if hop.TotalAmtMsat != 0 { + totalAmt = sql.NullInt64{ + Int64: int64(hop.TotalAmtMsat), + Valid: true, + } + } + + err := sqlDB.InsertRouteHopBlinded( + ctx, sqlc.InsertRouteHopBlindedParams{ + HopID: hopID, + EncryptedData: hop.EncryptedData, + BlindingPoint: blindingPoint, + BlindedPathTotalAmt: totalAmt, + }, + ) + if err != nil { + return fmt.Errorf("insert blinded hop: %w", err) + } + } + + // Check for MPP record. + if hop.MPP != nil { + paymentAddr := hop.MPP.PaymentAddr() + err = sqlDB.InsertRouteHopMpp(ctx, sqlc.InsertRouteHopMppParams{ + HopID: hopID, + PaymentAddr: paymentAddr[:], + TotalMsat: int64(hop.MPP.TotalMsat()), + }) + if err != nil { + return fmt.Errorf("insert MPP: %w", err) + } + } + + // Check for AMP record. + if hop.AMP != nil { + rootShare := hop.AMP.RootShare() + setID := hop.AMP.SetID() + err = sqlDB.InsertRouteHopAmp(ctx, sqlc.InsertRouteHopAmpParams{ + HopID: hopID, + RootShare: rootShare[:], + SetID: setID[:], + ChildIndex: int32(hop.AMP.ChildIndex()), + }) + if err != nil { + return fmt.Errorf("insert AMP: %w", err) + } + } + + // Check for custom records. + if hop.CustomRecords != nil { + for tlvType, value := range hop.CustomRecords { + err = sqlDB.InsertPaymentHopCustomRecord(ctx, + sqlc.InsertPaymentHopCustomRecordParams{ + HopID: hopID, + Key: int64(tlvType), + Value: value, + }) + if err != nil { + return fmt.Errorf("insert hop custom "+ + "record: %w", err) + } + } + } + + stats.TotalHops++ + + return nil +} + +// migrateDuplicatePayments migrates duplicate payments into the dedicated +// payment_duplicates table. +func migrateDuplicatePayments(ctx context.Context, dupBucket kvdb.RBucket, + hash [32]byte, primaryPaymentID int64, sqlDB SQLQueries, + stats *MigrationStats) error { + + duplicateCount := 0 + + err := dupBucket.ForEach(func(seqBytes, _ []byte) error { + // The duplicates bucket should only contain nested buckets + // keyed by 8-byte sequence numbers. Skip any unexpected keys + // (defensive check for corrupted or malformed data). + if len(seqBytes) != 8 { + log.Warnf("Skipping unexpected key in duplicates "+ + "bucket for payment %x: key length %d, "+ + "expected 8", + hash[:8], len(seqBytes), + ) + + return nil + } + + seqNum := byteOrder.Uint64(seqBytes) + subBucket := dupBucket.NestedReadBucket(seqBytes) + if subBucket == nil { + return nil + } + + duplicateCount++ + log.Infof("Migrating duplicate payment seq=%d for "+ + "payment %x", seqNum, hash[:8]) + + err := migrateSingleDuplicatePayment( + ctx, subBucket, hash, primaryPaymentID, seqNum, + sqlDB, + ) + if err != nil { + return fmt.Errorf( + "migrate duplicate payment seq=%d: %w", + seqNum, err, + ) + } + + return nil + }) + + if duplicateCount > 0 { + stats.DuplicatePayments++ + stats.DuplicateEntries += int64(duplicateCount) + + log.Infof("Payment %x had %d duplicate(s) migrated", hash[:8], + duplicateCount) + } + + return err +} + +// migrateSingleDuplicatePayment inserts a duplicate payment record for the +// given payment hash into payment_duplicates. +func migrateSingleDuplicatePayment(ctx context.Context, dupBucket kvdb.RBucket, + hash [32]byte, primaryPaymentID int64, duplicateSeq uint64, + sqlDB SQLQueries) error { + + creationData := dupBucket.Get(duplicatePaymentCreationInfoKey) + if creationData == nil { + return fmt.Errorf("duplicate payment seq=%d missing "+ + "creation info (payment=%x)", duplicateSeq, hash[:8]) + } + + creationInfo, err := deserializeDuplicatePaymentCreationInfo( + bytes.NewReader(creationData), + ) + if err != nil { + return fmt.Errorf("deserialize duplicate creation "+ + "info: %w", err) + } + + settleData := dupBucket.Get(duplicatePaymentSettleInfoKey) + failReasonData := dupBucket.Get(duplicatePaymentFailInfoKey) + attemptData := dupBucket.Get(duplicatePaymentAttemptInfoKey) + + if settleData != nil && len(failReasonData) > 0 { + return fmt.Errorf("duplicate payment seq=%d has both "+ + "settle and fail info (payment=%x)", duplicateSeq, + hash[:8]) + } + + var ( + failReason sql.NullInt32 + settlePreimage []byte + settleTime sql.NullTime + ) + + switch { + case settleData != nil: + settlePreimage, settleTime, err = parseDuplicateSettleData( + settleData, + ) + if err != nil { + return err + } + + case len(failReasonData) > 0: + failReason = sql.NullInt32{ + Int32: int32(failReasonData[0]), + Valid: true, + } + + default: + if attemptData == nil { + log.Warnf("Duplicate payment seq=%d has no "+ + "attempt info and no resolution (payment=%x); "+ + "marking failed", duplicateSeq, hash[:8]) + } else { + log.Warnf("Duplicate payment seq=%d has attempt "+ + "info but no resolution (payment=%x); "+ + "marking failed", duplicateSeq, hash[:8]) + } + + failReason = sql.NullInt32{ + Int32: int32(FailureReasonError), + Valid: true, + } + } + + _, err = sqlDB.InsertPaymentDuplicateMig( + ctx, sqlc.InsertPaymentDuplicateMigParams{ + PaymentID: primaryPaymentID, + PaymentIdentifier: creationInfo.PaymentIdentifier[:], + AmountMsat: int64(creationInfo.Value), + CreatedAt: normalizeTimeForSQL( + creationInfo.CreationTime, + ), + FailReason: failReason, + SettlePreimage: settlePreimage, + SettleTime: settleTime, + }, + ) + if err != nil { + return fmt.Errorf("insert duplicate payment: %w", err) + } + + return nil +} + +// parseDuplicateSettleData extracts settle data from either legacy or modern +// duplicate formats. +func parseDuplicateSettleData(settleData []byte) ([]byte, sql.NullTime, error) { + if len(settleData) == lntypes.PreimageSize { + return append([]byte(nil), settleData...), sql.NullTime{}, nil + } + + settleInfo, err := deserializeHTLCSettleInfo( + bytes.NewReader(settleData), + ) + if err != nil { + return nil, sql.NullTime{}, + fmt.Errorf("deserialize duplicate settle: %w", err) + } + + settleTime := normalizeTimeForSQL(settleInfo.SettleTime) + + return settleInfo.Preimage[:], sql.NullTime{ + Time: settleTime, + Valid: !settleTime.IsZero(), + }, nil +} + +// printMigrationSummary prints a summary of the migration. +func printMigrationSummary(stats *MigrationStats) { + log.Infof("========================================") + log.Infof(" Payment Migration Summary") + log.Infof("========================================") + log.Infof("Total Payments: %d", stats.TotalPayments) + log.Infof(" Successful: %d", stats.SuccessfulPayments) + log.Infof(" Failed: %d", stats.FailedPayments) + log.Infof(" In-Flight: %d", stats.InFlightPayments) + log.Infof("") + log.Infof("Total HTLC Attempts: %d", stats.TotalAttempts) + log.Infof(" Settled: %d", stats.SettledAttempts) + log.Infof(" Failed: %d", stats.FailedAttempts) + log.Infof(" In-Flight: %d", stats.InFlightAttempts) + log.Infof("") + log.Infof("Total Route Hops: %d", stats.TotalHops) + + if stats.DuplicatePayments > 0 { + log.Infof("") + log.Warnf("DUPLICATE PAYMENTS DETECTED:") + log.Warnf(" Unique payment hashes with duplicates: %d", + stats.DuplicatePayments) + log.Warnf(" Total duplicate entries migrated: %d", + stats.DuplicateEntries) + log.Warnf(" These were caused by an old LND bug.") + } + + log.Infof("") + log.Infof("Migration Duration: %v", stats.MigrationDuration) + log.Infof("========================================") +} diff --git a/payments/db/migration1/sql_migration_test.go b/payments/db/migration1/sql_migration_test.go new file mode 100644 index 00000000000..a36470d5f66 --- /dev/null +++ b/payments/db/migration1/sql_migration_test.go @@ -0,0 +1,2781 @@ +//go:build test_db_postgres || test_db_sqlite + +package migration1 + +import ( + "bytes" + "context" + "crypto/sha256" + "fmt" + "io" + "sort" + "testing" + "time" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/lightningnetwork/lnd/kvdb" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/payments/db/migration1/sqlc" + "github.com/lightningnetwork/lnd/record" + "github.com/lightningnetwork/lnd/routing/route" + "github.com/lightningnetwork/lnd/sqldb" + "github.com/stretchr/testify/require" +) + +// TestMigrationKVToSQL tests the basic payment migration from KV to SQL. +func TestMigrationKVToSQL(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + // Setup KV database and populate with test data. + kvDB := setupTestKVDB(t) + populateTestPayments(t, kvDB, 5) + + sqlStore := setupTestSQLDB(t) + + // Run migration in a single transaction. + err := sqlStore.db.ExecTx( + ctx, sqldb.WriteTxOpt(), func(tx SQLQueries) error { + return MigratePaymentsKVToSQL(ctx, kvDB, tx, + &SQLStoreConfig{ + QueryCfg: sqlStore.cfg.QueryCfg, + }, + ) + }, sqldb.NoOpReset, + ) + require.NoError(t, err) +} + +// TestMigrationSequenceOrder ensures the migration follows sequence order +// rather than lexicographic hash order. +func TestMigrationSequenceOrder(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + kvDB := setupTestKVDB(t) + err := kvdb.Update(kvDB, func(tx kvdb.RwTx) error { + paymentsBucket, err := tx.CreateTopLevelBucket( + paymentsRootBucket, + ) + if err != nil { + return err + } + + indexBucket, err := tx.CreateTopLevelBucket(paymentsIndexBucket) + if err != nil { + return err + } + + var globalAttemptID uint64 + hash0 := [32]byte{} + hash1 := [32]byte{} + hash2 := [32]byte{} + hash0[0] = 3 + hash1[0] = 2 + hash2[0] = 1 + + if err := createTestPaymentInKV( + t, paymentsBucket, indexBucket, 0, hash0, + &globalAttemptID, + ); err != nil { + return err + } + + // We make sure that the duplicate payment is skipped because + // it will be migrated separately into payment_duplicates. + if err := createTestDuplicatePaymentWithIndex( + t, paymentsBucket, indexBucket, hash0, 1, false, + &globalAttemptID, + ); err != nil { + return err + } + if err := createTestPaymentInKV( + t, paymentsBucket, indexBucket, 2, hash1, + &globalAttemptID, + ); err != nil { + return err + } + if err := createTestPaymentInKV( + t, paymentsBucket, indexBucket, 3, hash2, + &globalAttemptID, + ); err != nil { + return err + } + + return nil + }, func() {}) + require.NoError(t, err) + + sqlStore := setupTestSQLDB(t) + + err = sqlStore.db.ExecTx( + ctx, sqldb.WriteTxOpt(), func(tx SQLQueries) error { + return MigratePaymentsKVToSQL( + ctx, kvDB, tx, &SQLStoreConfig{ + QueryCfg: sqlStore.cfg.QueryCfg, + }, + ) + }, sqldb.NoOpReset, + ) + require.NoError(t, err) + + resp, err := sqlStore.QueryPayments(ctx, Query{ + MaxPayments: 10, + IncludeIncomplete: true, + }) + require.NoError(t, err) + require.Len(t, resp.Payments, 3) + + var ( + exp0 lntypes.Hash + exp1 lntypes.Hash + exp2 lntypes.Hash + ) + exp0[0] = 3 + exp1[0] = 2 + exp2[0] = 1 + + require.Equal(t, exp0, resp.Payments[0].Info.PaymentIdentifier) + require.Equal(t, exp1, resp.Payments[1].Info.PaymentIdentifier) + require.Equal(t, exp2, resp.Payments[2].Info.PaymentIdentifier) +} + +// TestMigrationDataIntegrity verifies that migrated payment data exactly +// matches the original KV data when fetched through the SQLStore +// (SQLStore.FetchPayment). This covers the SQLStore query path separately +// from the migration's own batch validation. +func TestMigrationDataIntegrity(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + // Setup KV database with test data. + kvDB := setupTestKVDB(t) + numPayments := populateTestPayments(t, kvDB, 5) + + // Fetch all payments from KV before migration. + kvPayments := fetchAllPaymentsFromKV(t, kvDB) + require.Len(t, kvPayments, numPayments) + + // Setup SQL database and run migration. + sqlStore := setupTestSQLDB(t) + + err := sqlStore.db.ExecTx( + ctx, sqldb.WriteTxOpt(), func(tx SQLQueries) error { + return MigratePaymentsKVToSQL( + ctx, kvDB, tx, &SQLStoreConfig{ + QueryCfg: sqlStore.cfg.QueryCfg, + }, + ) + }, sqldb.NoOpReset, + ) + require.NoError(t, err) + + // Compare each KV payment with its SQL counterpart using deep equality. + // This ensures that ALL fields match, not just a few selected ones. + for _, kvPayment := range kvPayments { + comparePaymentData(t, ctx, sqlStore, kvPayment) + } +} + +// TestMigrationWithDuplicates tests migration of duplicate payments into +// the payment_duplicates table. +func TestMigrationWithDuplicates(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + // Setup KV database. + kvDB := setupTestKVDB(t) + + // Create a payment with duplicates. + hash := createTestPaymentHash(t, 0) + err := kvdb.Update(kvDB, func(tx kvdb.RwTx) error { + // Create root buckets. + paymentsBucket, err := tx.CreateTopLevelBucket( + paymentsRootBucket, + ) + if err != nil { + return err + } + + indexBucket, err := tx.CreateTopLevelBucket(paymentsIndexBucket) + if err != nil { + return err + } + + // Create primary payment with sequence 0 and globally unique + // attempt ID. + var globalAttemptID uint64 + err = createTestPaymentInKV( + t, paymentsBucket, indexBucket, 0, hash, + &globalAttemptID, + ) + if err != nil { + return err + } + + // Add 2 duplicate payments for the same hash. + paymentBucket := paymentsBucket.NestedReadWriteBucket(hash[:]) + require.NotNil(t, paymentBucket) + + dupBucket, err := paymentBucket.CreateBucketIfNotExists( + duplicatePaymentsBucket, + ) + if err != nil { + return err + } + + // Create duplicate with sequence 1 using global attempt ID. + err = createTestDuplicatePayment( + t, dupBucket, hash, 1, true, &globalAttemptID, + ) + if err != nil { + return err + } + + // Create duplicate with sequence 2 using global attempt ID. + err = createTestDuplicatePayment( + t, dupBucket, hash, 2, false, &globalAttemptID, + ) + if err != nil { + return err + } + + return nil + }, func() {}) + require.NoError(t, err) + + sqlStore := setupTestSQLDB(t) + + // Run migration. + err = sqlStore.db.ExecTx( + ctx, sqldb.WriteTxOpt(), func(tx SQLQueries) error { + return MigratePaymentsKVToSQL( + ctx, kvDB, tx, &SQLStoreConfig{ + QueryCfg: sqlStore.cfg.QueryCfg, + }, + ) + }, sqldb.NoOpReset, + ) + require.NoError(t, err) + + // Verify in SQL database. + var count int64 + err = sqlStore.db.ExecTx( + ctx, sqldb.ReadTxOpt(), func(q SQLQueries) error { + var err error + count, err = q.CountPayments(ctx) + return err + }, sqldb.NoOpReset, + ) + require.NoError(t, err) + require.Equal( + t, int64(1), count, "SQL DB should have 1 payment", + ) + + var ( + dbPayment sqlc.FetchPaymentRow + duplicates []sqlc.PaymentDuplicate + ) + err = sqlStore.db.ExecTx( + ctx, sqldb.ReadTxOpt(), func(q SQLQueries) error { + var err error + dbPayment, err = q.FetchPayment(ctx, hash[:]) + if err != nil { + return err + } + + duplicates, err = q.FetchPaymentDuplicates( + ctx, dbPayment.Payment.ID, + ) + return err + }, sqldb.NoOpReset, + ) + require.NoError(t, err) + + require.Len(t, duplicates, 2) + sort.SliceStable(duplicates, func(i, j int) bool { + return duplicates[i].AmountMsat < duplicates[j].AmountMsat + }) + + require.Equal(t, hash[:], duplicates[0].PaymentIdentifier) + require.Equal(t, int64(2001000), duplicates[0].AmountMsat) + require.False(t, duplicates[0].FailReason.Valid) + require.NotEmpty(t, duplicates[0].SettlePreimage) + + require.Equal(t, hash[:], duplicates[1].PaymentIdentifier) + require.Equal(t, int64(2002000), duplicates[1].AmountMsat) + require.True(t, duplicates[1].FailReason.Valid) + require.Equal( + t, int32(FailureReasonError), + duplicates[1].FailReason.Int32, + ) + require.Empty(t, duplicates[1].SettlePreimage) +} + +// TestDuplicatePaymentsWithoutAttemptInfo verifies duplicate payments without +// attempt info are migrated with terminal failure reasons. +func TestDuplicatePaymentsWithoutAttemptInfo(t *testing.T) { + t.Parallel() + + ctx := context.Background() + kvDB := setupTestKVDB(t) + + hash := createTestPaymentHash(t, 0) + + err := kvdb.Update(kvDB, func(tx kvdb.RwTx) error { + paymentsBucket, err := tx.CreateTopLevelBucket( + paymentsRootBucket, + ) + if err != nil { + return err + } + + indexBucket, err := tx.CreateTopLevelBucket(paymentsIndexBucket) + if err != nil { + return err + } + + var globalAttemptID uint64 + err = createTestPaymentInKV( + t, paymentsBucket, indexBucket, 1, hash, + &globalAttemptID, + ) + if err != nil { + return err + } + + paymentBucket := paymentsBucket.NestedReadWriteBucket( + hash[:], + ) + require.NotNil(t, paymentBucket) + + dupBucket, err := paymentBucket.CreateBucketIfNotExists( + duplicatePaymentsBucket, + ) + if err != nil { + return err + } + + if err := createDuplicateWithoutAttemptInfo( + t, dupBucket, hash, 2, true, false, + ); err != nil { + return err + } + if err := createDuplicateWithoutAttemptInfo( + t, dupBucket, hash, 3, false, true, + ); err != nil { + return err + } + if err := createDuplicateWithoutAttemptInfo( + t, dupBucket, hash, 4, false, false, + ); err != nil { + return err + } + + return nil + }, func() {}) + require.NoError(t, err) + + sqlStore := setupTestSQLDB(t) + err = sqlStore.db.ExecTx( + ctx, sqldb.WriteTxOpt(), func(tx SQLQueries) error { + return MigratePaymentsKVToSQL( + ctx, kvDB, tx, &SQLStoreConfig{ + QueryCfg: sqlStore.cfg.QueryCfg, + }, + ) + }, sqldb.NoOpReset, + ) + require.NoError(t, err) + + var ( + dbPayment sqlc.FetchPaymentRow + duplicates []sqlc.PaymentDuplicate + ) + err = sqlStore.db.ExecTx( + ctx, sqldb.ReadTxOpt(), func(q SQLQueries) error { + var err error + dbPayment, err = q.FetchPayment(ctx, hash[:]) + if err != nil { + return err + } + + duplicates, err = q.FetchPaymentDuplicates( + ctx, dbPayment.Payment.ID, + ) + return err + }, sqldb.NoOpReset, + ) + require.NoError(t, err) + + require.Len(t, duplicates, 3) + sort.SliceStable(duplicates, func(i, j int) bool { + return duplicates[i].AmountMsat < duplicates[j].AmountMsat + }) + + require.Equal(t, int64(2002000), duplicates[0].AmountMsat) + require.NotEmpty(t, duplicates[0].SettlePreimage) + require.False(t, duplicates[0].FailReason.Valid) + + require.Equal(t, int64(2003000), duplicates[1].AmountMsat) + require.True(t, duplicates[1].FailReason.Valid) + require.Equal( + t, int32(FailureReasonNoRoute), + duplicates[1].FailReason.Int32, + ) + require.Empty(t, duplicates[1].SettlePreimage) + + require.Equal(t, int64(2004000), duplicates[2].AmountMsat) + require.True(t, duplicates[2].FailReason.Valid) + require.Equal( + t, int32(FailureReasonError), + duplicates[2].FailReason.Int32, + ) + require.Empty(t, duplicates[2].SettlePreimage) +} + +// TestMigratePaymentWithMPP tests migration of a payment with MPP (multi-path +// payment) records. +func TestMigratePaymentWithMPP(t *testing.T) { + t.Parallel() + + ctx := context.Background() + kvDB := setupTestKVDB(t) + + // Create a payment with MPP. + var paymentHash [32]byte + copy(paymentHash[:], []byte("test_mpp_payment_hash_12345")) + + err := kvdb.Update(kvDB, func(tx kvdb.RwTx) error { + paymentsBucket, err := tx.CreateTopLevelBucket( + paymentsRootBucket, + ) + if err != nil { + return err + } + + indexBucket, err := tx.CreateTopLevelBucket(paymentsIndexBucket) + if err != nil { + return err + } + + return createPaymentWithMPP( + t, paymentsBucket, indexBucket, paymentHash, + ) + }, func() {}) + require.NoError(t, err) + + // Run migration. + sqlStore := setupTestSQLDB(t) + + err = sqlStore.db.ExecTx( + ctx, sqldb.WriteTxOpt(), func(tx SQLQueries) error { + return MigratePaymentsKVToSQL( + ctx, kvDB, tx, &SQLStoreConfig{ + QueryCfg: sqlStore.cfg.QueryCfg, + }, + ) + }, sqldb.NoOpReset, + ) + require.NoError(t, err) + + // Verify payment matches. + assertPaymentDataMatches(t, ctx, kvDB, sqlStore, paymentHash) +} + +// TestMigratePaymentWithAMP tests migration of a payment with AMP (atomic +// multi-path) records. +func TestMigratePaymentWithAMP(t *testing.T) { + t.Parallel() + + ctx := context.Background() + kvDB := setupTestKVDB(t) + + var paymentHash [32]byte + copy(paymentHash[:], []byte("test_amp_payment_hash_12345")) + + err := kvdb.Update(kvDB, func(tx kvdb.RwTx) error { + paymentsBucket, err := tx.CreateTopLevelBucket( + paymentsRootBucket, + ) + if err != nil { + return err + } + + indexBucket, err := tx.CreateTopLevelBucket(paymentsIndexBucket) + if err != nil { + return err + } + + return createPaymentWithAMP( + t, paymentsBucket, indexBucket, paymentHash, + ) + }, func() {}) + require.NoError(t, err) + + sqlStore := setupTestSQLDB(t) + + err = sqlStore.db.ExecTx( + ctx, sqldb.WriteTxOpt(), func(tx SQLQueries) error { + return MigratePaymentsKVToSQL( + ctx, kvDB, tx, &SQLStoreConfig{ + QueryCfg: sqlStore.cfg.QueryCfg, + }, + ) + }, sqldb.NoOpReset, + ) + require.NoError(t, err) + + assertPaymentDataMatches(t, ctx, kvDB, sqlStore, paymentHash) +} + +// TestMigratePaymentWithAMPSignedChildIndex tests migration of an AMP payment +// where the child index has the signed bit set. +func TestMigratePaymentWithAMPSignedChildIndex(t *testing.T) { + t.Parallel() + + ctx := context.Background() + kvDB := setupTestKVDB(t) + + var paymentHash [32]byte + copy(paymentHash[:], []byte("test_amp_child_idx_8000")) + + const childIndex = uint32(0x80000001) + + err := kvdb.Update(kvDB, func(tx kvdb.RwTx) error { + paymentsBucket, err := tx.CreateTopLevelBucket( + paymentsRootBucket, + ) + if err != nil { + return err + } + + indexBucket, err := tx.CreateTopLevelBucket(paymentsIndexBucket) + if err != nil { + return err + } + + return createPaymentWithAMPChildIndex( + t, paymentsBucket, indexBucket, paymentHash, childIndex, + ) + }, func() {}) + require.NoError(t, err) + + sqlStore := setupTestSQLDB(t) + + err = sqlStore.db.ExecTx( + ctx, sqldb.WriteTxOpt(), func(tx SQLQueries) error { + return MigratePaymentsKVToSQL( + ctx, kvDB, tx, &SQLStoreConfig{ + QueryCfg: sqlStore.cfg.QueryCfg, + }, + ) + }, sqldb.NoOpReset, + ) + require.NoError(t, err) + + assertPaymentDataMatches(t, ctx, kvDB, sqlStore, paymentHash) +} + +// TestMigratePaymentWithCustomRecords tests migration of a payment with custom +// records. +func TestMigratePaymentWithCustomRecords(t *testing.T) { + t.Parallel() + + ctx := context.Background() + kvDB := setupTestKVDB(t) + + var paymentHash [32]byte + copy(paymentHash[:], []byte("test_custom_records_hash_12")) + + err := kvdb.Update(kvDB, func(tx kvdb.RwTx) error { + paymentsBucket, err := tx.CreateTopLevelBucket( + paymentsRootBucket, + ) + if err != nil { + return err + } + + indexBucket, err := tx.CreateTopLevelBucket(paymentsIndexBucket) + if err != nil { + return err + } + + return createPaymentWithCustomRecords( + t, paymentsBucket, indexBucket, paymentHash, + ) + }, func() {}) + require.NoError(t, err) + + sqlStore := setupTestSQLDB(t) + + err = sqlStore.db.ExecTx( + ctx, sqldb.WriteTxOpt(), func(tx SQLQueries) error { + return MigratePaymentsKVToSQL( + ctx, kvDB, tx, &SQLStoreConfig{ + QueryCfg: sqlStore.cfg.QueryCfg, + }, + ) + }, sqldb.NoOpReset, + ) + require.NoError(t, err) + + assertPaymentDataMatches(t, ctx, kvDB, sqlStore, paymentHash) +} + +// TestMigratePaymentWithBlindedRoute tests migration of a payment with blinded +// route. +func TestMigratePaymentWithBlindedRoute(t *testing.T) { + t.Parallel() + + ctx := context.Background() + kvDB := setupTestKVDB(t) + + var paymentHash [32]byte + copy(paymentHash[:], []byte("test_blinded_route_hash_123")) + + err := kvdb.Update(kvDB, func(tx kvdb.RwTx) error { + paymentsBucket, err := tx.CreateTopLevelBucket( + paymentsRootBucket, + ) + if err != nil { + return err + } + + indexBucket, err := tx.CreateTopLevelBucket(paymentsIndexBucket) + if err != nil { + return err + } + + return createPaymentWithBlindedRoute( + t, paymentsBucket, indexBucket, paymentHash, + ) + }, func() {}) + require.NoError(t, err) + + sqlStore := setupTestSQLDB(t) + + err = sqlStore.db.ExecTx( + ctx, sqldb.WriteTxOpt(), func(tx SQLQueries) error { + return MigratePaymentsKVToSQL( + ctx, kvDB, tx, &SQLStoreConfig{ + QueryCfg: sqlStore.cfg.QueryCfg, + }, + ) + }, sqldb.NoOpReset, + ) + require.NoError(t, err) + + assertPaymentDataMatches(t, ctx, kvDB, sqlStore, paymentHash) +} + +// TestMigratePaymentWithMetadata tests migration of a payment with hop +// metadata. +func TestMigratePaymentWithMetadata(t *testing.T) { + t.Parallel() + + ctx := context.Background() + kvDB := setupTestKVDB(t) + + var paymentHash [32]byte + copy(paymentHash[:], []byte("test_metadata_payment_hash_")) + + err := kvdb.Update(kvDB, func(tx kvdb.RwTx) error { + paymentsBucket, err := tx.CreateTopLevelBucket( + paymentsRootBucket, + ) + if err != nil { + return err + } + + indexBucket, err := tx.CreateTopLevelBucket(paymentsIndexBucket) + if err != nil { + return err + } + + return createPaymentWithMetadata( + t, paymentsBucket, indexBucket, paymentHash, + ) + }, func() {}) + require.NoError(t, err) + + sqlStore := setupTestSQLDB(t) + + err = sqlStore.db.ExecTx( + ctx, sqldb.WriteTxOpt(), func(tx SQLQueries) error { + return MigratePaymentsKVToSQL( + ctx, kvDB, tx, &SQLStoreConfig{ + QueryCfg: sqlStore.cfg.QueryCfg, + }, + ) + }, sqldb.NoOpReset, + ) + require.NoError(t, err) + + assertPaymentDataMatches(t, ctx, kvDB, sqlStore, paymentHash) +} + +// TestMigratePaymentWithAllFeatures tests migration with all optional +// features enabled. +func TestMigratePaymentWithAllFeatures(t *testing.T) { + t.Parallel() + + ctx := context.Background() + kvDB := setupTestKVDB(t) + + var paymentHash [32]byte + copy(paymentHash[:], []byte("test_all_features_hash_1234")) + + err := kvdb.Update(kvDB, func(tx kvdb.RwTx) error { + paymentsBucket, err := tx.CreateTopLevelBucket( + paymentsRootBucket, + ) + if err != nil { + return err + } + + indexBucket, err := tx.CreateTopLevelBucket(paymentsIndexBucket) + if err != nil { + return err + } + + return createPaymentWithAllFeatures( + t, paymentsBucket, indexBucket, paymentHash, + ) + }, func() {}) + require.NoError(t, err) + + sqlStore := setupTestSQLDB(t) + + err = sqlStore.db.ExecTx( + ctx, sqldb.WriteTxOpt(), func(tx SQLQueries) error { + return MigratePaymentsKVToSQL( + ctx, kvDB, tx, &SQLStoreConfig{ + QueryCfg: sqlStore.cfg.QueryCfg, + }, + ) + }, sqldb.NoOpReset, + ) + require.NoError(t, err) + + assertPaymentDataMatches(t, ctx, kvDB, sqlStore, paymentHash) +} + +// TestMigratePaymentFeatureCombinations tests selected feature combinations +// in a single migration to cover interactions without random data. +func TestMigratePaymentFeatureCombinations(t *testing.T) { + t.Parallel() + + ctx := context.Background() + kvDB := setupTestKVDB(t) + + cases := []paymentFeatureSet{ + { + name: "mpp_custom", + mpp: true, + customRecords: true, + }, + { + name: "amp_blinded", + amp: true, + blindedRoute: true, + }, + { + name: "custom_metadata", + customRecords: true, + hopMetadata: true, + }, + { + name: "blinded_metadata", + blindedRoute: true, + hopMetadata: true, + }, + { + name: "mpp_metadata", + mpp: true, + hopMetadata: true, + }, + } + + hashes := make([][32]byte, 0, len(cases)) + err := kvdb.Update(kvDB, func(tx kvdb.RwTx) error { + paymentsBucket, err := tx.CreateTopLevelBucket( + paymentsRootBucket, + ) + if err != nil { + return err + } + + indexBucket, err := tx.CreateTopLevelBucket(paymentsIndexBucket) + if err != nil { + return err + } + + var globalAttemptID uint64 + for i, c := range cases { + hash := sha256.Sum256([]byte(c.name)) + hashes = append(hashes, hash) + + err := createPaymentWithFeatureSet( + t, paymentsBucket, indexBucket, hash, + uint64(10+i), c, &globalAttemptID, + ) + if err != nil { + return err + } + } + + return nil + }, func() {}) + require.NoError(t, err) + + sqlStore := setupTestSQLDB(t) + + err = sqlStore.db.ExecTx( + ctx, sqldb.WriteTxOpt(), func(tx SQLQueries) error { + return MigratePaymentsKVToSQL( + ctx, kvDB, tx, &SQLStoreConfig{ + QueryCfg: sqlStore.cfg.QueryCfg, + }, + ) + }, sqldb.NoOpReset, + ) + require.NoError(t, err) + + for _, hash := range hashes { + assertPaymentDataMatches(t, ctx, kvDB, sqlStore, hash) + } +} + +// TestMigratePaymentWithFailureMessage tests migration of a payment with a +// failed HTLC that includes a failure message. +func TestMigratePaymentWithFailureMessage(t *testing.T) { + t.Parallel() + + ctx := context.Background() + kvDB := setupTestKVDB(t) + + var paymentHash [32]byte + copy(paymentHash[:], []byte("test_fail_msg_hash_123456789")) + + // Create a payment with a failed HTLC + err := kvdb.Update(kvDB, func(tx kvdb.RwTx) error { + paymentsBucket, err := tx.CreateTopLevelBucket( + paymentsRootBucket, + ) + if err != nil { + return err + } + + indexBucket, err := tx.CreateTopLevelBucket(paymentsIndexBucket) + if err != nil { + return err + } + + // Create payment bucket + paymentBucket, err := paymentsBucket.CreateBucketIfNotExists( + paymentHash[:], + ) + if err != nil { + return err + } + + // Add creation info + var paymentID lntypes.Hash + copy(paymentID[:], paymentHash[:]) + + creationInfo := &PaymentCreationInfo{ + PaymentIdentifier: paymentID, + Value: lnwire.MilliSatoshi(1000000), + CreationTime: time.Now().Add(-24 * time.Hour), + PaymentRequest: []byte("lnbc10utest"), + } + + // Use a separate buffer for payment creation info to avoid + // reuse issues when serializing HTLC attempts later. + var creationInfoBuf bytes.Buffer + err = serializePaymentCreationInfo( + &creationInfoBuf, creationInfo, + ) + if err != nil { + return err + } + + serialized := creationInfoBuf.Bytes() + + err = paymentBucket.Put( + paymentCreationInfoKey, serialized, + ) + if err != nil { + return err + } + + // Add sequence number + seqBytes := make([]byte, 8) + byteOrder.PutUint64(seqBytes, 50) + err = paymentBucket.Put(paymentSequenceKey, seqBytes) + if err != nil { + return err + } + + // Add payment-level failure reason + failReasonBytes := []byte{byte(FailureReasonNoRoute)} + err = paymentBucket.Put( + paymentFailInfoKey, failReasonBytes, + ) + if err != nil { + return err + } + + // Create HTLC bucket with one failed attempt + htlcBucket, err := paymentBucket.CreateBucketIfNotExists( + paymentHtlcsBucket, + ) + if err != nil { + return err + } + + // Create the failed attempt with a failure message + attemptID := uint64(500) + sessionKey, err := btcec.NewPrivateKey() + if err != nil { + return err + } + + var sessionKeyBytes [32]byte + copy(sessionKeyBytes[:], sessionKey.Serialize()) + + var sourcePubKey route.Vertex + copy(sourcePubKey[:], sessionKey.PubKey().SerializeCompressed()) + + hopKey, err := btcec.NewPrivateKey() + if err != nil { + return err + } + + // Create a proper copy of the hash instead of referencing + // the local variable directly. + attemptHash := new(lntypes.Hash) + copy(attemptHash[:], paymentHash[:]) + + //nolint:ll + attemptInfo := &HTLCAttemptInfo{ + AttemptID: attemptID, + sessionKey: sessionKeyBytes, + Route: route.Route{ + TotalTimeLock: 500000, + TotalAmount: 900, + SourcePubKey: sourcePubKey, + Hops: []*route.Hop{ + { + PubKeyBytes: route.NewVertex(hopKey.PubKey()), + ChannelID: 12345, + OutgoingTimeLock: 499500, + AmtToForward: 850, + }, + }, + }, + AttemptTime: time.Now().Add(-2 * time.Hour), + Hash: attemptHash, + } + + // Write attempt info + attemptKey := make([]byte, len(htlcAttemptInfoKey)+8) + copy(attemptKey, htlcAttemptInfoKey) + byteOrder.PutUint64( + attemptKey[len(htlcAttemptInfoKey):], attemptID, + ) + + var b bytes.Buffer + err = serializeHTLCAttemptInfo(&b, attemptInfo) + if err != nil { + return err + } + err = htlcBucket.Put(attemptKey, b.Bytes()) + if err != nil { + return err + } + + // Add failure info with a message + //nolint:ll + failInfo := &HTLCFailInfo{ + FailTime: time.Now().Add(-1 * time.Hour), + Message: &lnwire.FailTemporaryChannelFailure{}, + Reason: HTLCFailMessage, + FailureSourceIndex: 1, + } + + failKey := make([]byte, len(htlcFailInfoKey)+8) + copy(failKey, htlcFailInfoKey) + byteOrder.PutUint64(failKey[len(htlcFailInfoKey):], attemptID) + + b.Reset() + if err := serializeHTLCFailInfo(&b, failInfo); err != nil { + return err + } + if err := htlcBucket.Put(failKey, b.Bytes()); err != nil { + return err + } + + // Create index entry. + var idx bytes.Buffer + if err := WriteElements( + &idx, paymentIndexTypeHash, paymentHash[:], + ); err != nil { + return err + } + return indexBucket.Put(seqBytes, idx.Bytes()) + }, func() {}) + require.NoError(t, err) + + // Migrate to SQL + sqlStore := setupTestSQLDB(t) + + err = sqlStore.db.ExecTx( + ctx, sqldb.WriteTxOpt(), func(tx SQLQueries) error { + return MigratePaymentsKVToSQL( + ctx, kvDB, tx, &SQLStoreConfig{ + QueryCfg: sqlStore.cfg.QueryCfg, + }, + ) + }, sqldb.NoOpReset, + ) + require.NoError(t, err) + + // Verify data matches. + assertPaymentDataMatches(t, ctx, kvDB, sqlStore, paymentHash) +} + +// setupTestKVDB creates a temporary KV database for testing. +func setupTestKVDB(t *testing.T) kvdb.Backend { + t.Helper() + + backend, cleanup, err := kvdb.GetTestBackend(t.TempDir(), "payments") + require.NoError(t, err) + t.Cleanup(cleanup) + + return backend +} + +// populateTestPayments populates the KV database with test payment data. +func populateTestPayments(t *testing.T, db kvdb.Backend, numPayments int) int { + t.Helper() + + err := kvdb.Update(db, func(tx kvdb.RwTx) error { + // Create root buckets. + paymentsBucket, err := tx.CreateTopLevelBucket( + paymentsRootBucket, + ) + if err != nil { + return err + } + + indexBucket, err := tx.CreateTopLevelBucket(paymentsIndexBucket) + if err != nil { + return err + } + + // Create test payments with globally unique attempt IDs. + var globalAttemptID uint64 + for i := 0; i < numPayments; i++ { + hash := createTestPaymentHash(t, i) + + err := createTestPaymentInKV( + t, paymentsBucket, indexBucket, uint64(i), hash, + &globalAttemptID, + ) + if err != nil { + return err + } + } + + return nil + }, func() {}) + + require.NoError(t, err) + return numPayments +} + +// serializeDuplicatePaymentCreationInfo serializes PaymentCreationInfo for +// duplicate payments. The time is stored in seconds (not nanoseconds) to match +// the format used by deserializeDuplicatePaymentCreationInfo in the KV store. +func serializeDuplicatePaymentCreationInfo(w io.Writer, + c *PaymentCreationInfo) error { + + var scratch [8]byte + + if _, err := w.Write(c.PaymentIdentifier[:]); err != nil { + return err + } + + byteOrder.PutUint64(scratch[:], uint64(c.Value)) + if _, err := w.Write(scratch[:]); err != nil { + return err + } + + // Store time in seconds (not nanoseconds) for duplicate payments. + // This matches the deserialization format used in + // deserializeDuplicatePaymentCreationInfo. + var unixSec int64 + if !c.CreationTime.IsZero() { + unixSec = c.CreationTime.Unix() + } + byteOrder.PutUint64(scratch[:], uint64(unixSec)) + if _, err := w.Write(scratch[:]); err != nil { + return err + } + + byteOrder.PutUint32(scratch[:4], uint32(len(c.PaymentRequest))) + if _, err := w.Write(scratch[:4]); err != nil { + return err + } + + if _, err := w.Write(c.PaymentRequest); err != nil { + return err + } + + return nil +} + +// createTestPaymentHash creates a deterministic payment hash for testing. +func createTestPaymentHash(t *testing.T, seed int) [32]byte { + t.Helper() + + hash := sha256.Sum256([]byte{byte(seed)}) + return hash +} + +// createTestPaymentInKV creates a single payment in the KV store. +func createTestPaymentInKV(t *testing.T, paymentsBucket, + indexBucket kvdb.RwBucket, seqNum uint64, hash [32]byte, + globalAttemptID *uint64) error { + + t.Helper() + + // Create payment bucket. + paymentBucket, err := paymentsBucket.CreateBucketIfNotExists(hash[:]) + if err != nil { + return err + } + + // Create payment creation info. + var paymentID lntypes.Hash + copy(paymentID[:], hash[:]) + + creationInfo := &PaymentCreationInfo{ + PaymentIdentifier: paymentID, + Value: lnwire.MilliSatoshi(1000000 + seqNum*1000), + CreationTime: time.Now().Add(-24 * time.Hour), + PaymentRequest: []byte("lnbc1test"), + } + + // Serialize and write creation info. + var b bytes.Buffer + err = serializePaymentCreationInfo(&b, creationInfo) + if err != nil { + return err + } + err = paymentBucket.Put(paymentCreationInfoKey, b.Bytes()) + if err != nil { + return err + } + + // Store sequence number. + seqBytes := make([]byte, 8) + byteOrder.PutUint64(seqBytes, seqNum) + err = paymentBucket.Put(paymentSequenceKey, seqBytes) + if err != nil { + return err + } + + // Add one HTLC attempt for each payment with globally unique ID. + htlcBucket, err := paymentBucket.CreateBucketIfNotExists( + paymentHtlcsBucket, + ) + if err != nil { + return err + } + + // Increment global attempt ID and create HTLC attempt. So we have a + // globally unique attempt ID for the HTLC attempt. + *globalAttemptID++ + err = createTestHTLCAttempt( + t, htlcBucket, hash, *globalAttemptID, seqNum%3 == 0, + ) + if err != nil { + return err + } + + var idx bytes.Buffer + err = WriteElements(&idx, paymentIndexTypeHash, hash[:]) + if err != nil { + return err + } + + return indexBucket.Put(seqBytes, idx.Bytes()) +} + +// createTestHTLCAttempt creates a test HTLC attempt in the KV store. +func createTestHTLCAttempt(t *testing.T, htlcBucket kvdb.RwBucket, + paymentHash [32]byte, attemptID uint64, shouldSettle bool) error { + t.Helper() + + // Generate a session key. + sessionKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + // Create a simple 2-hop route. + hop1Key, err := btcec.NewPrivateKey() + require.NoError(t, err) + + hop2Key, err := btcec.NewPrivateKey() + require.NoError(t, err) + + var sourcePubKey route.Vertex + copy(sourcePubKey[:], sessionKey.PubKey().SerializeCompressed()) + + // Convert session key to [32]byte. + var sessionKeyBytes [32]byte + copy(sessionKeyBytes[:], sessionKey.Serialize()) + + attemptInfo := &HTLCAttemptInfo{ + AttemptID: attemptID, + sessionKey: sessionKeyBytes, + Route: route.Route{ + TotalTimeLock: 500000, + TotalAmount: 900, + SourcePubKey: sourcePubKey, + Hops: []*route.Hop{ + { + PubKeyBytes: route.NewVertex( + hop1Key.PubKey(), + ), + ChannelID: 12345, + OutgoingTimeLock: 499500, + AmtToForward: 850, + }, + { + PubKeyBytes: route.NewVertex( + hop2Key.PubKey(), + ), + ChannelID: 67890, + OutgoingTimeLock: 499000, + AmtToForward: 800, + }, + }, + }, + AttemptTime: time.Now().Add(-2 * time.Hour), + Hash: (*lntypes.Hash)(&paymentHash), + } + + // Serialize and write attempt info. + attemptKey := make([]byte, len(htlcAttemptInfoKey)+8) + copy(attemptKey, htlcAttemptInfoKey) + byteOrder.PutUint64(attemptKey[len(htlcAttemptInfoKey):], attemptID) + + var b bytes.Buffer + err = serializeHTLCAttemptInfo(&b, attemptInfo) + if err != nil { + return err + } + err = htlcBucket.Put(attemptKey, b.Bytes()) + if err != nil { + return err + } + + // Add settlement if requested. + if shouldSettle { + settleInfo := &HTLCSettleInfo{ + Preimage: lntypes.Preimage(paymentHash), + SettleTime: time.Now().Add(-1 * time.Hour), + } + + settleKey := make([]byte, len(htlcSettleInfoKey)+8) + copy(settleKey, htlcSettleInfoKey) + byteOrder.PutUint64( + settleKey[len(htlcSettleInfoKey):], attemptID, + ) + + var sb bytes.Buffer + err = serializeHTLCSettleInfo(&sb, settleInfo) + if err != nil { + return err + } + err = htlcBucket.Put(settleKey, sb.Bytes()) + if err != nil { + return err + } + } + + return nil +} + +// createTestDuplicatePaymentWithIndex creates a duplicate payment and adds +// a matching entry into the global payment sequence index. +func createTestDuplicatePaymentWithIndex(t *testing.T, + paymentsBucket kvdb.RwBucket, indexBucket kvdb.RwBucket, + paymentHash [32]byte, seqNum uint64, shouldSettle bool, + globalAttemptID *uint64) error { + t.Helper() + + paymentBucket, err := paymentsBucket.CreateBucketIfNotExists( + paymentHash[:], + ) + if err != nil { + return err + } + + dupBucket, err := paymentBucket.CreateBucketIfNotExists( + duplicatePaymentsBucket, + ) + if err != nil { + return err + } + + if err := createTestDuplicatePayment( + t, dupBucket, paymentHash, seqNum, shouldSettle, + globalAttemptID, + ); err != nil { + return err + } + + seqBytes := make([]byte, 8) + byteOrder.PutUint64(seqBytes, seqNum) + var idx bytes.Buffer + if err := WriteElements( + &idx, paymentIndexTypeHash, paymentHash[:], + ); err != nil { + return err + } + + return indexBucket.Put(seqBytes, idx.Bytes()) +} + +// createTestDuplicatePayment creates a duplicate payment in the KV store. +func createTestDuplicatePayment(t *testing.T, + dupBucket kvdb.RwBucket, paymentHash [32]byte, seqNum uint64, + shouldSettle bool, globalAttemptID *uint64) error { + + t.Helper() + + // Create bucket for this duplicate using sequence number as key. + seqBytes := make([]byte, 8) + byteOrder.PutUint64(seqBytes, seqNum) + + dupPaymentBucket, err := dupBucket.CreateBucketIfNotExists(seqBytes) + if err != nil { + return err + } + + // Store sequence number. + err = dupPaymentBucket.Put(duplicatePaymentSequenceKey, seqBytes) + if err != nil { + return err + } + + // Create payment creation info. + var paymentID lntypes.Hash + copy(paymentID[:], paymentHash[:]) + + creationInfo := &PaymentCreationInfo{ + PaymentIdentifier: paymentID, + Value: lnwire.MilliSatoshi(2000000 + seqNum*1000), + CreationTime: time.Now().Add(-48 * time.Hour), + PaymentRequest: []byte("lnbc1duplicate"), + } + + var b bytes.Buffer + err = serializeDuplicatePaymentCreationInfo(&b, creationInfo) + if err != nil { + return err + } + err = dupPaymentBucket.Put(duplicatePaymentCreationInfoKey, b.Bytes()) + if err != nil { + return err + } + + // Generate a session key for the duplicate attempt. + sessionKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + // Create route for duplicate. + hop1Key, err := btcec.NewPrivateKey() + require.NoError(t, err) + + hop2Key, err := btcec.NewPrivateKey() + require.NoError(t, err) + + var sourcePubKey route.Vertex + copy(sourcePubKey[:], sessionKey.PubKey().SerializeCompressed()) + + var sessionKeyBytes [32]byte + copy(sessionKeyBytes[:], sessionKey.Serialize()) + + // Use globally unique attempt ID. + *globalAttemptID++ + attemptID := *globalAttemptID + + duplicateAttempt := &duplicateHTLCAttemptInfo{ + attemptID: attemptID, + sessionKey: sessionKeyBytes, + route: route.Route{ + TotalTimeLock: 500000, + TotalAmount: 900, + SourcePubKey: sourcePubKey, + Hops: []*route.Hop{ + { + PubKeyBytes: route.NewVertex( + hop1Key.PubKey(), + ), + ChannelID: 12345, + OutgoingTimeLock: 499500, + AmtToForward: 850, + }, + { + PubKeyBytes: route.NewVertex( + hop2Key.PubKey(), + ), + ChannelID: 67890, + OutgoingTimeLock: 499000, + AmtToForward: 800, + }, + }, + }, + } + + // Serialize and write attempt info (using existing WriteElements + // and SerializeRoute). + var ab bytes.Buffer + if err := WriteElements( + &ab, duplicateAttempt.attemptID, + duplicateAttempt.sessionKey, + ); err != nil { + return err + } + if err := SerializeRoute(&ab, duplicateAttempt.route); err != nil { + return err + } + err = dupPaymentBucket.Put(duplicatePaymentAttemptInfoKey, ab.Bytes()) + if err != nil { + return err + } + + // Add settlement if requested. + if shouldSettle { + settleInfo := &HTLCSettleInfo{ + Preimage: lntypes.Preimage(paymentHash), + SettleTime: time.Now().Add(-1 * time.Hour), + } + + var sb bytes.Buffer + err = serializeHTLCSettleInfo(&sb, settleInfo) + if err != nil { + return err + } + err = dupPaymentBucket.Put( + duplicatePaymentSettleInfoKey, sb.Bytes(), + ) + if err != nil { + return err + } + } + + return nil +} + +// createDuplicateWithoutAttemptInfo creates a duplicate payment bucket with +// settle/fail info but without attempt info. +func createDuplicateWithoutAttemptInfo(t *testing.T, + dupBucket kvdb.RwBucket, paymentHash [32]byte, seqNum uint64, + shouldSettle bool, shouldFail bool) error { + t.Helper() + + seqBytes := make([]byte, 8) + byteOrder.PutUint64(seqBytes, seqNum) + + dupPaymentBucket, err := dupBucket.CreateBucketIfNotExists(seqBytes) + if err != nil { + return err + } + + if err := dupPaymentBucket.Put( + duplicatePaymentSequenceKey, seqBytes, + ); err != nil { + return err + } + + var paymentID lntypes.Hash + copy(paymentID[:], paymentHash[:]) + + creationInfo := &PaymentCreationInfo{ + PaymentIdentifier: paymentID, + Value: lnwire.MilliSatoshi(2000000 + seqNum*1000), + CreationTime: time.Now().Add(-48 * time.Hour), + PaymentRequest: []byte("lnbc1duplicate"), + } + + var b bytes.Buffer + err = serializeDuplicatePaymentCreationInfo(&b, creationInfo) + if err != nil { + return err + } + if err := dupPaymentBucket.Put( + duplicatePaymentCreationInfoKey, b.Bytes(), + ); err != nil { + return err + } + + switch { + case shouldSettle && shouldFail: + return fmt.Errorf("invalid duplicate state") + case shouldSettle: + settleInfo := &HTLCSettleInfo{ + Preimage: lntypes.Preimage(paymentHash), + SettleTime: time.Now().Add(-1 * time.Hour), + } + + var sb bytes.Buffer + err = serializeHTLCSettleInfo(&sb, settleInfo) + if err != nil { + return err + } + if err := dupPaymentBucket.Put( + duplicatePaymentSettleInfoKey, sb.Bytes(), + ); err != nil { + return err + } + case shouldFail: + failReasonBytes := []byte{byte(FailureReasonNoRoute)} + if err := dupPaymentBucket.Put( + duplicatePaymentFailInfoKey, failReasonBytes, + ); err != nil { + return err + } + } + + return nil +} + +// fetchAllPaymentsFromKV fetches all payments from the KV store using the +// KVStore implementation. +func fetchAllPaymentsFromKV(t *testing.T, kvDB kvdb.Backend) []*MPPayment { + t.Helper() + + kvStore, err := NewKVStore(kvDB, WithNoMigration(true)) + require.NoError(t, err) + + payments, err := kvStore.FetchPayments() + require.NoError(t, err) + + return payments +} + +// normalizePaymentData makes sure that the payment data is normalized for +// comparison. We align to local time and truncate to microsecond precision +// (matching invoice migration behavior). +func normalizePaymentData(payment *MPPayment) { + trunc := func(t time.Time) time.Time { + if t.IsZero() { + return t + } + return t.In(time.Local).Truncate(time.Microsecond) + } + + // SequenceNum is a database ID which will differ between KV and SQL. + payment.SequenceNum = 0 + + // Normalize payment creation time. + payment.Info.CreationTime = trunc(payment.Info.CreationTime) + + // Normalize payment-level custom records. + if len(payment.Info.FirstHopCustomRecords) == 0 { + payment.Info.FirstHopCustomRecords = nil + } + + // Normalize HTLC attempt times. + // Normalize nil vs empty slice for HTLCs. + // SQL returns empty slice for payments with no attempts, + // while KV returns nil. + if payment.HTLCs == nil { + payment.HTLCs = []HTLCAttempt{} + return + } + + for i := range payment.HTLCs { + payment.HTLCs[i].AttemptTime = trunc( + payment.HTLCs[i].AttemptTime, + ) + + if payment.HTLCs[i].Settle != nil { + payment.HTLCs[i].Settle.SettleTime = trunc( + payment.HTLCs[i].Settle.SettleTime, + ) + } + + if payment.HTLCs[i].Failure != nil { + payment.HTLCs[i].Failure.FailTime = trunc( + payment.HTLCs[i].Failure.FailTime, + ) + } + + // Zero out non-serialized cached fields (onionBlob and + // circuit). These are computed on-demand and not stored in the + // database. + payment.HTLCs[i].onionBlob = [1366]byte{} + payment.HTLCs[i].circuit = nil + payment.HTLCs[i].cachedSessionKey = nil + + // Normalize route-level custom records. + if len(payment.HTLCs[i].Route.FirstHopWireCustomRecords) == 0 { + payment.HTLCs[i].Route.FirstHopWireCustomRecords = nil + } + + // Normalize hop-level custom records. + hops := payment.HTLCs[i].Route.Hops + for j := range hops { + if len(hops[j].CustomRecords) == 0 { + hops[j].CustomRecords = nil + } + } + } +} + +// comparePaymentData compares a KV payment with its SQL counterpart using +// deep equality check (similar to invoice migration). +func comparePaymentData(t *testing.T, ctx context.Context, sqlStore *SQLStore, + kvPayment *MPPayment) { + + t.Helper() + + // Fetch the SQL payment as MPPayment using SQLStore. + var paymentHash lntypes.Hash + copy(paymentHash[:], kvPayment.Info.PaymentIdentifier[:]) + + sqlPayment, err := sqlStore.FetchPayment(ctx, paymentHash) + require.NoError(t, err, "SQL payment should exist for %x", + paymentHash[:8]) + + // Normalize time precision to microseconds. + normalizePaymentData(kvPayment) + normalizePaymentData(sqlPayment) + + // Deep equality check - compares all fields recursively. + require.Equal(t, kvPayment, sqlPayment, + "KV and SQL payments should be equal for %x", paymentHash[:8]) +} + +// assertPaymentDataMatches verifies a payment in KV matches its SQL counterpart +// using deep equality check. +func assertPaymentDataMatches(t *testing.T, ctx context.Context, + kvDB kvdb.Backend, sqlStore *SQLStore, hash [32]byte) { + t.Helper() + + // Fetch from KV. + var kvPayment *MPPayment + err := kvdb.View(kvDB, func(tx kvdb.RTx) error { + paymentsBucket := tx.ReadBucket(paymentsRootBucket) + if paymentsBucket == nil { + return nil + } + + paymentBucket := paymentsBucket.NestedReadBucket(hash[:]) + if paymentBucket == nil { + return nil + } + + var err error + kvPayment, err = fetchPayment(paymentBucket) + return err + }, func() {}) + require.NoError(t, err) + + if kvPayment == nil { + // Payment doesn't exist in KV, should not exist in SQL + // either. + var paymentHash lntypes.Hash + copy(paymentHash[:], hash[:]) + _, err := sqlStore.FetchPayment(ctx, paymentHash) + require.Error( + t, err, "payment should not exist in SQL if not "+ + "in KV", + ) + return + } + + // Use the deep comparison function. + comparePaymentData(t, ctx, sqlStore, kvPayment) +} + +// createPaymentWithMPP creates a payment with MPP records on the final hop. +func createPaymentWithMPP(t *testing.T, paymentsBucket, + indexBucket kvdb.RwBucket, hash [32]byte) error { + t.Helper() + + paymentBucket, err := paymentsBucket.CreateBucketIfNotExists(hash[:]) + if err != nil { + return err + } + + // Create payment info. + var paymentID lntypes.Hash + copy(paymentID[:], hash[:]) + + creationInfo := &PaymentCreationInfo{ + PaymentIdentifier: paymentID, + Value: lnwire.MilliSatoshi(50000), + CreationTime: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC), + PaymentRequest: []byte("lnbc500n1test_mpp"), + } + + var b bytes.Buffer + err = serializePaymentCreationInfo(&b, creationInfo) + if err != nil { + return err + } + err = paymentBucket.Put(paymentCreationInfoKey, b.Bytes()) + if err != nil { + return err + } + + // Store sequence number. + seqBytes := make([]byte, 8) + byteOrder.PutUint64(seqBytes, 1) + err = paymentBucket.Put(paymentSequenceKey, seqBytes) + if err != nil { + return err + } + + // Create HTLC with MPP. + htlcBucket, err := paymentBucket.CreateBucketIfNotExists( + paymentHtlcsBucket, + ) + if err != nil { + return err + } + + sessionKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + var sourcePubKey route.Vertex + copy(sourcePubKey[:], sessionKey.PubKey().SerializeCompressed()) + + var sessionKeyBytes [32]byte + copy(sessionKeyBytes[:], sessionKey.Serialize()) + + // Create route with 3 hops, MPP on final hop. + hops := make([]*route.Hop, 3) + for i := 0; i < 3; i++ { + hopKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + amt := lnwire.MilliSatoshi(50000 - uint64(i)*100) + hop := &route.Hop{ + PubKeyBytes: route.NewVertex(hopKey.PubKey()), + ChannelID: uint64(100000 + i), + OutgoingTimeLock: uint32(500000 - i*40), + AmtToForward: amt, + } + + // Add MPP to final hop. + if i == 2 { + var paymentAddr [32]byte + copy( + paymentAddr[:], + []byte("test_mpp_payment_address_32"), + ) + hop.MPP = record.NewMPP( + lnwire.MilliSatoshi(50000), + paymentAddr, + ) + } + + hops[i] = hop + } + + attemptInfo := &HTLCAttemptInfo{ + AttemptID: 1, + sessionKey: sessionKeyBytes, + Route: route.Route{ + TotalTimeLock: 500000, + TotalAmount: lnwire.MilliSatoshi(49800), + SourcePubKey: sourcePubKey, + Hops: hops, + }, + AttemptTime: time.Date(2024, 1, 1, 12, 1, 0, 0, time.UTC), + Hash: (*lntypes.Hash)(&hash), + } + + attemptKey := make([]byte, len(htlcAttemptInfoKey)+8) + copy(attemptKey, htlcAttemptInfoKey) + byteOrder.PutUint64(attemptKey[len(htlcAttemptInfoKey):], 1) + + var ab bytes.Buffer + err = serializeHTLCAttemptInfo(&ab, attemptInfo) + if err != nil { + return err + } + err = htlcBucket.Put(attemptKey, ab.Bytes()) + if err != nil { + return err + } + + // Add settlement. + settleInfo := &HTLCSettleInfo{ + Preimage: lntypes.Preimage(hash), + SettleTime: time.Date(2024, 1, 1, 12, 2, 0, 0, time.UTC), + } + + settleKey := make([]byte, len(htlcSettleInfoKey)+8) + copy(settleKey, htlcSettleInfoKey) + byteOrder.PutUint64(settleKey[len(htlcSettleInfoKey):], 1) + + var sb bytes.Buffer + err = serializeHTLCSettleInfo(&sb, settleInfo) + if err != nil { + return err + } + err = htlcBucket.Put(settleKey, sb.Bytes()) + if err != nil { + return err + } + + // Create index entry. + var idx bytes.Buffer + err = WriteElements(&idx, paymentIndexTypeHash, hash[:]) + if err != nil { + return err + } + return indexBucket.Put(seqBytes, idx.Bytes()) +} + +// createPaymentWithAMP creates a payment with AMP records on the final hop. +func createPaymentWithAMP(t *testing.T, paymentsBucket, + indexBucket kvdb.RwBucket, hash [32]byte) error { + t.Helper() + + return createPaymentWithAMPChildIndex( + t, paymentsBucket, indexBucket, hash, 0, + ) +} + +// createPaymentWithAMPChildIndex creates a payment with AMP records on the +// final hop and a specific child index. +func createPaymentWithAMPChildIndex(t *testing.T, paymentsBucket, + indexBucket kvdb.RwBucket, hash [32]byte, childIndex uint32) error { + t.Helper() + + paymentBucket, err := paymentsBucket.CreateBucketIfNotExists(hash[:]) + if err != nil { + return err + } + + var paymentID lntypes.Hash + copy(paymentID[:], hash[:]) + + creationInfo := &PaymentCreationInfo{ + PaymentIdentifier: paymentID, + Value: lnwire.MilliSatoshi(75000), + CreationTime: time.Date(2024, 2, 1, 10, 0, 0, 0, time.UTC), + PaymentRequest: []byte("lnbc750n1test_amp"), + } + + var b bytes.Buffer + err = serializePaymentCreationInfo(&b, creationInfo) + if err != nil { + return err + } + err = paymentBucket.Put(paymentCreationInfoKey, b.Bytes()) + if err != nil { + return err + } + + seqBytes := make([]byte, 8) + byteOrder.PutUint64(seqBytes, 2) + err = paymentBucket.Put(paymentSequenceKey, seqBytes) + if err != nil { + return err + } + + htlcBucket, err := paymentBucket.CreateBucketIfNotExists( + paymentHtlcsBucket, + ) + if err != nil { + return err + } + + sessionKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + var sourcePubKey route.Vertex + copy(sourcePubKey[:], sessionKey.PubKey().SerializeCompressed()) + + var sessionKeyBytes [32]byte + copy(sessionKeyBytes[:], sessionKey.Serialize()) + + // Create route with AMP on final hop. + hops := make([]*route.Hop, 2) + for i := 0; i < 2; i++ { + hopKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + amt := lnwire.MilliSatoshi(75000 - uint64(i)*50) + hop := &route.Hop{ + PubKeyBytes: route.NewVertex(hopKey.PubKey()), + ChannelID: uint64(200000 + i), + OutgoingTimeLock: uint32(600000 - i*40), + AmtToForward: amt, + } + + // Add AMP to final hop. + if i == 1 { + var rootShare [32]byte + copy( + rootShare[:], + []byte("test_amp_root_share_12345678"), + ) + var setID [32]byte + copy(setID[:], []byte("test_amp_set_id_123456789012")) + hop.AMP = record.NewAMP(rootShare, setID, childIndex) + } + + hops[i] = hop + } + + attemptInfo := &HTLCAttemptInfo{ + AttemptID: 1, + sessionKey: sessionKeyBytes, + Route: route.Route{ + TotalTimeLock: 600000, + TotalAmount: lnwire.MilliSatoshi(74950), + SourcePubKey: sourcePubKey, + Hops: hops, + }, + AttemptTime: time.Date(2024, 2, 1, 10, 1, 0, 0, time.UTC), + Hash: (*lntypes.Hash)(&hash), + } + + attemptKey := make([]byte, len(htlcAttemptInfoKey)+8) + copy(attemptKey, htlcAttemptInfoKey) + byteOrder.PutUint64(attemptKey[len(htlcAttemptInfoKey):], 1) + + var ab bytes.Buffer + err = serializeHTLCAttemptInfo(&ab, attemptInfo) + if err != nil { + return err + } + err = htlcBucket.Put(attemptKey, ab.Bytes()) + if err != nil { + return err + } + + // Add settlement. + settleInfo := &HTLCSettleInfo{ + Preimage: lntypes.Preimage(hash), + SettleTime: time.Date(2024, 2, 1, 10, 2, 0, 0, time.UTC), + } + + settleKey := make([]byte, len(htlcSettleInfoKey)+8) + copy(settleKey, htlcSettleInfoKey) + byteOrder.PutUint64(settleKey[len(htlcSettleInfoKey):], 1) + + var sb bytes.Buffer + err = serializeHTLCSettleInfo(&sb, settleInfo) + if err != nil { + return err + } + err = htlcBucket.Put(settleKey, sb.Bytes()) + if err != nil { + return err + } + + var idx bytes.Buffer + err = WriteElements(&idx, paymentIndexTypeHash, hash[:]) + if err != nil { + return err + } + return indexBucket.Put(seqBytes, idx.Bytes()) +} + +// createPaymentWithCustomRecords creates a payment with custom records at all +// levels. +func createPaymentWithCustomRecords(t *testing.T, paymentsBucket, + indexBucket kvdb.RwBucket, hash [32]byte) error { + t.Helper() + + paymentBucket, err := paymentsBucket.CreateBucketIfNotExists(hash[:]) + if err != nil { + return err + } + + var paymentID lntypes.Hash + copy(paymentID[:], hash[:]) + + // Payment-level custom records. + creationInfo := &PaymentCreationInfo{ + PaymentIdentifier: paymentID, + Value: lnwire.MilliSatoshi(100000), + CreationTime: time.Date(2024, 3, 1, 14, 0, 0, 0, time.UTC), + PaymentRequest: []byte("lnbc1m1test_custom"), + FirstHopCustomRecords: lnwire.CustomRecords{ + 65536: []byte("payment_level_value_1"), + 65537: []byte("payment_level_value_2"), + }, + } + + var b bytes.Buffer + err = serializePaymentCreationInfo(&b, creationInfo) + if err != nil { + return err + } + err = paymentBucket.Put(paymentCreationInfoKey, b.Bytes()) + if err != nil { + return err + } + + seqBytes := make([]byte, 8) + byteOrder.PutUint64(seqBytes, 3) + err = paymentBucket.Put(paymentSequenceKey, seqBytes) + if err != nil { + return err + } + + htlcBucket, err := paymentBucket.CreateBucketIfNotExists( + paymentHtlcsBucket, + ) + if err != nil { + return err + } + + sessionKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + var sourcePubKey route.Vertex + copy(sourcePubKey[:], sessionKey.PubKey().SerializeCompressed()) + + var sessionKeyBytes [32]byte + copy(sessionKeyBytes[:], sessionKey.Serialize()) + + // Create route with custom records at all levels. + hops := make([]*route.Hop, 3) + for i := 0; i < 3; i++ { + hopKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + amt := lnwire.MilliSatoshi(100000 - uint64(i)*150) + hop := &route.Hop{ + PubKeyBytes: route.NewVertex(hopKey.PubKey()), + ChannelID: uint64(300000 + i), + OutgoingTimeLock: uint32(700000 - i*40), + AmtToForward: amt, + // Hop-level custom records. + CustomRecords: record.CustomSet{ + 65538 + uint64(i): []byte( + fmt.Sprintf("hop_%d_custom_value", i), + ), + }, + } + + hops[i] = hop + } + + attemptInfo := &HTLCAttemptInfo{ + AttemptID: 1, + sessionKey: sessionKeyBytes, + Route: route.Route{ + TotalTimeLock: 700000, + TotalAmount: lnwire.MilliSatoshi(99700), + SourcePubKey: sourcePubKey, + Hops: hops, + // Attempt-level first hop custom records. + FirstHopWireCustomRecords: lnwire.CustomRecords{ + 65541: []byte("attempt_custom_value_1"), + 65542: []byte("attempt_custom_value_2"), + }, + }, + AttemptTime: time.Date(2024, 3, 1, 14, 1, 0, 0, time.UTC), + Hash: (*lntypes.Hash)(&hash), + } + + attemptKey := make([]byte, len(htlcAttemptInfoKey)+8) + copy(attemptKey, htlcAttemptInfoKey) + byteOrder.PutUint64(attemptKey[len(htlcAttemptInfoKey):], 1) + + var ab bytes.Buffer + err = serializeHTLCAttemptInfo(&ab, attemptInfo) + if err != nil { + return err + } + err = htlcBucket.Put(attemptKey, ab.Bytes()) + if err != nil { + return err + } + + settleInfo := &HTLCSettleInfo{ + Preimage: lntypes.Preimage(hash), + SettleTime: time.Date(2024, 3, 1, 14, 2, 0, 0, time.UTC), + } + + settleKey := make([]byte, len(htlcSettleInfoKey)+8) + copy(settleKey, htlcSettleInfoKey) + byteOrder.PutUint64(settleKey[len(htlcSettleInfoKey):], 1) + + var sb bytes.Buffer + err = serializeHTLCSettleInfo(&sb, settleInfo) + if err != nil { + return err + } + err = htlcBucket.Put(settleKey, sb.Bytes()) + if err != nil { + return err + } + + var idx bytes.Buffer + err = WriteElements(&idx, paymentIndexTypeHash, hash[:]) + if err != nil { + return err + } + return indexBucket.Put(seqBytes, idx.Bytes()) +} + +// createPaymentWithBlindedRoute creates a payment with blinded route data. +func createPaymentWithBlindedRoute(t *testing.T, paymentsBucket, + indexBucket kvdb.RwBucket, hash [32]byte) error { + t.Helper() + + paymentBucket, err := paymentsBucket.CreateBucketIfNotExists(hash[:]) + if err != nil { + return err + } + + var paymentID lntypes.Hash + copy(paymentID[:], hash[:]) + + creationInfo := &PaymentCreationInfo{ + PaymentIdentifier: paymentID, + Value: lnwire.MilliSatoshi(120000), + CreationTime: time.Date(2024, 4, 1, 16, 0, 0, 0, time.UTC), + PaymentRequest: []byte("lnbc1200n1test_blinded"), + } + + var b bytes.Buffer + err = serializePaymentCreationInfo(&b, creationInfo) + if err != nil { + return err + } + err = paymentBucket.Put(paymentCreationInfoKey, b.Bytes()) + if err != nil { + return err + } + + seqBytes := make([]byte, 8) + byteOrder.PutUint64(seqBytes, 4) + err = paymentBucket.Put(paymentSequenceKey, seqBytes) + if err != nil { + return err + } + + htlcBucket, err := paymentBucket.CreateBucketIfNotExists( + paymentHtlcsBucket, + ) + if err != nil { + return err + } + + sessionKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + var sourcePubKey route.Vertex + copy(sourcePubKey[:], sessionKey.PubKey().SerializeCompressed()) + + var sessionKeyBytes [32]byte + copy(sessionKeyBytes[:], sessionKey.Serialize()) + + // Create route with blinded data on final hop. + hops := make([]*route.Hop, 4) + for i := 0; i < 4; i++ { + hopKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + amt := lnwire.MilliSatoshi(120000 - uint64(i)*200) + hop := &route.Hop{ + PubKeyBytes: route.NewVertex(hopKey.PubKey()), + ChannelID: uint64(400000 + i), + OutgoingTimeLock: uint32(800000 - i*40), + AmtToForward: amt, + } + + // Add blinded route data to final hop. + if i == 3 { + blindingKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + hop.BlindingPoint = blindingKey.PubKey() + hop.EncryptedData = []byte( + "encrypted_blinded_route_data_test_value_12345", + ) + hop.TotalAmtMsat = lnwire.MilliSatoshi(119400) + } + + hops[i] = hop + } + + attemptInfo := &HTLCAttemptInfo{ + AttemptID: 1, + sessionKey: sessionKeyBytes, + Route: route.Route{ + TotalTimeLock: 800000, + TotalAmount: lnwire.MilliSatoshi(119400), + SourcePubKey: sourcePubKey, + Hops: hops, + }, + AttemptTime: time.Date(2024, 4, 1, 16, 1, 0, 0, time.UTC), + Hash: (*lntypes.Hash)(&hash), + } + + attemptKey := make([]byte, len(htlcAttemptInfoKey)+8) + copy(attemptKey, htlcAttemptInfoKey) + byteOrder.PutUint64(attemptKey[len(htlcAttemptInfoKey):], 1) + + var ab bytes.Buffer + err = serializeHTLCAttemptInfo(&ab, attemptInfo) + if err != nil { + return err + } + err = htlcBucket.Put(attemptKey, ab.Bytes()) + if err != nil { + return err + } + + settleInfo := &HTLCSettleInfo{ + Preimage: lntypes.Preimage(hash), + SettleTime: time.Date(2024, 4, 1, 16, 2, 0, 0, time.UTC), + } + + settleKey := make([]byte, len(htlcSettleInfoKey)+8) + copy(settleKey, htlcSettleInfoKey) + byteOrder.PutUint64(settleKey[len(htlcSettleInfoKey):], 1) + + var sb bytes.Buffer + err = serializeHTLCSettleInfo(&sb, settleInfo) + if err != nil { + return err + } + err = htlcBucket.Put(settleKey, sb.Bytes()) + if err != nil { + return err + } + + var idx bytes.Buffer + err = WriteElements(&idx, paymentIndexTypeHash, hash[:]) + if err != nil { + return err + } + return indexBucket.Put(seqBytes, idx.Bytes()) +} + +// createPaymentWithMetadata creates a payment with hop metadata. +func createPaymentWithMetadata(t *testing.T, paymentsBucket, + indexBucket kvdb.RwBucket, hash [32]byte) error { + t.Helper() + + paymentBucket, err := paymentsBucket.CreateBucketIfNotExists(hash[:]) + if err != nil { + return err + } + + var paymentID lntypes.Hash + copy(paymentID[:], hash[:]) + + creationInfo := &PaymentCreationInfo{ + PaymentIdentifier: paymentID, + Value: lnwire.MilliSatoshi(80000), + CreationTime: time.Date(2024, 5, 1, 18, 0, 0, 0, time.UTC), + PaymentRequest: []byte("lnbc800n1test_metadata"), + } + + var b bytes.Buffer + err = serializePaymentCreationInfo(&b, creationInfo) + if err != nil { + return err + } + err = paymentBucket.Put(paymentCreationInfoKey, b.Bytes()) + if err != nil { + return err + } + + seqBytes := make([]byte, 8) + byteOrder.PutUint64(seqBytes, 5) + err = paymentBucket.Put(paymentSequenceKey, seqBytes) + if err != nil { + return err + } + + htlcBucket, err := paymentBucket.CreateBucketIfNotExists( + paymentHtlcsBucket, + ) + if err != nil { + return err + } + + sessionKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + var sourcePubKey route.Vertex + copy(sourcePubKey[:], sessionKey.PubKey().SerializeCompressed()) + + var sessionKeyBytes [32]byte + copy(sessionKeyBytes[:], sessionKey.Serialize()) + + // Create route with metadata on all hops. + hops := make([]*route.Hop, 3) + for i := 0; i < 3; i++ { + hopKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + amt := lnwire.MilliSatoshi(80000 - uint64(i)*100) + hop := &route.Hop{ + PubKeyBytes: route.NewVertex(hopKey.PubKey()), + ChannelID: uint64(500000 + i), + OutgoingTimeLock: uint32(900000 - i*40), + AmtToForward: amt, + Metadata: []byte( + fmt.Sprintf("hop_%d_metadata_value", i), + ), + } + + hops[i] = hop + } + + attemptInfo := &HTLCAttemptInfo{ + AttemptID: 1, + sessionKey: sessionKeyBytes, + Route: route.Route{ + TotalTimeLock: 900000, + TotalAmount: lnwire.MilliSatoshi(79800), + SourcePubKey: sourcePubKey, + Hops: hops, + }, + AttemptTime: time.Date(2024, 5, 1, 18, 1, 0, 0, time.UTC), + Hash: (*lntypes.Hash)(&hash), + } + + attemptKey := make([]byte, len(htlcAttemptInfoKey)+8) + copy(attemptKey, htlcAttemptInfoKey) + byteOrder.PutUint64(attemptKey[len(htlcAttemptInfoKey):], 1) + + var ab bytes.Buffer + err = serializeHTLCAttemptInfo(&ab, attemptInfo) + if err != nil { + return err + } + err = htlcBucket.Put(attemptKey, ab.Bytes()) + if err != nil { + return err + } + + settleInfo := &HTLCSettleInfo{ + Preimage: lntypes.Preimage(hash), + SettleTime: time.Date(2024, 5, 1, 18, 2, 0, 0, time.UTC), + } + + settleKey := make([]byte, len(htlcSettleInfoKey)+8) + copy(settleKey, htlcSettleInfoKey) + byteOrder.PutUint64(settleKey[len(htlcSettleInfoKey):], 1) + + var sb bytes.Buffer + err = serializeHTLCSettleInfo(&sb, settleInfo) + if err != nil { + return err + } + err = htlcBucket.Put(settleKey, sb.Bytes()) + if err != nil { + return err + } + + var idx bytes.Buffer + err = WriteElements(&idx, paymentIndexTypeHash, hash[:]) + if err != nil { + return err + } + return indexBucket.Put(seqBytes, idx.Bytes()) +} + +// createPaymentWithAllFeatures creates a payment with all optional features +// enabled. +func createPaymentWithAllFeatures(t *testing.T, paymentsBucket, + indexBucket kvdb.RwBucket, hash [32]byte) error { + t.Helper() + + paymentBucket, err := paymentsBucket.CreateBucketIfNotExists(hash[:]) + if err != nil { + return err + } + + var paymentID lntypes.Hash + copy(paymentID[:], hash[:]) + + // Payment with all features: payment-level custom records. + creationInfo := &PaymentCreationInfo{ + PaymentIdentifier: paymentID, + Value: lnwire.MilliSatoshi(150000), + CreationTime: time.Date(2024, 6, 1, 20, 0, 0, 0, time.UTC), + PaymentRequest: []byte("lnbc1500n1test_all_features"), + FirstHopCustomRecords: lnwire.CustomRecords{ + 65543: []byte("all_features_payment_custom_1"), + 65544: []byte("all_features_payment_custom_2"), + }, + } + + var b bytes.Buffer + err = serializePaymentCreationInfo(&b, creationInfo) + if err != nil { + return err + } + err = paymentBucket.Put(paymentCreationInfoKey, b.Bytes()) + if err != nil { + return err + } + + seqBytes := make([]byte, 8) + byteOrder.PutUint64(seqBytes, 6) + err = paymentBucket.Put(paymentSequenceKey, seqBytes) + if err != nil { + return err + } + + htlcBucket, err := paymentBucket.CreateBucketIfNotExists( + paymentHtlcsBucket, + ) + if err != nil { + return err + } + + sessionKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + var sourcePubKey route.Vertex + copy(sourcePubKey[:], sessionKey.PubKey().SerializeCompressed()) + + var sessionKeyBytes [32]byte + copy(sessionKeyBytes[:], sessionKey.Serialize()) + + // Create route with all features: MPP, custom records, blinded route, + // metadata. + hops := make([]*route.Hop, 4) + for i := 0; i < 4; i++ { + hopKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + amt := lnwire.MilliSatoshi(150000 - uint64(i)*250) + hop := &route.Hop{ + PubKeyBytes: route.NewVertex(hopKey.PubKey()), + ChannelID: uint64(600000 + i), + OutgoingTimeLock: uint32(1000000 - i*40), + AmtToForward: amt, + // Hop-level custom records. + CustomRecords: record.CustomSet{ + 65545 + uint64(i): []byte(fmt.Sprintf( + "all_feat_hop_%d", i, + )), + }, + // Hop metadata. + Metadata: []byte( + fmt.Sprintf("all_feat_metadata_%d", i), + ), + } + + // Add MPP and blinded route data to final hop. + if i == 3 { + var paymentAddr [32]byte + copy( + paymentAddr[:], + []byte("all_features_mpp_addr_123456"), + ) + hop.MPP = record.NewMPP( + lnwire.MilliSatoshi(149250), + paymentAddr, + ) + + blindingKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + hop.BlindingPoint = blindingKey.PubKey() + hop.EncryptedData = []byte( + "all_features_encrypted_blinded_data_123456", + ) + hop.TotalAmtMsat = lnwire.MilliSatoshi(149250) + } + + hops[i] = hop + } + + attemptInfo := &HTLCAttemptInfo{ + AttemptID: 1, + sessionKey: sessionKeyBytes, + Route: route.Route{ + TotalTimeLock: 1000000, + TotalAmount: lnwire.MilliSatoshi(149250), + SourcePubKey: sourcePubKey, + Hops: hops, + // Attempt-level first hop custom records. + FirstHopWireCustomRecords: lnwire.CustomRecords{ + 65549: []byte("all_feat_attempt_custom_1"), + 65550: []byte("all_feat_attempt_custom_2"), + }, + }, + AttemptTime: time.Date(2024, 6, 1, 20, 1, 0, 0, time.UTC), + Hash: (*lntypes.Hash)(&hash), + } + + attemptKey := make([]byte, len(htlcAttemptInfoKey)+8) + copy(attemptKey, htlcAttemptInfoKey) + byteOrder.PutUint64(attemptKey[len(htlcAttemptInfoKey):], 1) + + var ab bytes.Buffer + err = serializeHTLCAttemptInfo(&ab, attemptInfo) + if err != nil { + return err + } + err = htlcBucket.Put(attemptKey, ab.Bytes()) + if err != nil { + return err + } + + settleInfo := &HTLCSettleInfo{ + Preimage: lntypes.Preimage(hash), + SettleTime: time.Date(2024, 6, 1, 20, 2, 0, 0, time.UTC), + } + + settleKey := make([]byte, len(htlcSettleInfoKey)+8) + copy(settleKey, htlcSettleInfoKey) + byteOrder.PutUint64(settleKey[len(htlcSettleInfoKey):], 1) + + var sb bytes.Buffer + err = serializeHTLCSettleInfo(&sb, settleInfo) + if err != nil { + return err + } + err = htlcBucket.Put(settleKey, sb.Bytes()) + if err != nil { + return err + } + + var idx bytes.Buffer + err = WriteElements(&idx, paymentIndexTypeHash, hash[:]) + if err != nil { + return err + } + + return indexBucket.Put(seqBytes, idx.Bytes()) +} + +type paymentFeatureSet struct { + name string + mpp bool + amp bool + customRecords bool + blindedRoute bool + hopMetadata bool +} + +// createPaymentWithFeatureSet creates a payment with a selected set of +// optional features for combination testing. +func createPaymentWithFeatureSet(t *testing.T, paymentsBucket, + indexBucket kvdb.RwBucket, hash [32]byte, seqNum uint64, + features paymentFeatureSet, globalAttemptID *uint64) error { + t.Helper() + + if features.mpp && features.amp { + return fmt.Errorf("invalid feature set: mpp and amp") + } + + paymentBucket, err := paymentsBucket.CreateBucketIfNotExists(hash[:]) + if err != nil { + return err + } + + var paymentID lntypes.Hash + copy(paymentID[:], hash[:]) + + creationTime := time.Date(2024, 7, 1, 12, 0, 0, 0, time.UTC). + Add(time.Duration(seqNum) * time.Minute) + creationInfo := &PaymentCreationInfo{ + PaymentIdentifier: paymentID, + Value: lnwire.MilliSatoshi(100000), + CreationTime: creationTime, + PaymentRequest: []byte( + fmt.Sprintf("lnbc_test_%s", features.name), + ), + } + if features.customRecords { + creationInfo.FirstHopCustomRecords = lnwire.CustomRecords{ + 65560: []byte("combo_payment_custom_1"), + 65561: []byte("combo_payment_custom_2"), + } + } + + var b bytes.Buffer + err = serializePaymentCreationInfo(&b, creationInfo) + if err != nil { + return err + } + err = paymentBucket.Put(paymentCreationInfoKey, b.Bytes()) + if err != nil { + return err + } + + seqBytes := make([]byte, 8) + byteOrder.PutUint64(seqBytes, seqNum) + err = paymentBucket.Put(paymentSequenceKey, seqBytes) + if err != nil { + return err + } + + htlcBucket, err := paymentBucket.CreateBucketIfNotExists( + paymentHtlcsBucket, + ) + if err != nil { + return err + } + + sessionKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + var sourcePubKey route.Vertex + copy(sourcePubKey[:], sessionKey.PubKey().SerializeCompressed()) + + var sessionKeyBytes [32]byte + copy(sessionKeyBytes[:], sessionKey.Serialize()) + + baseAmt := lnwire.MilliSatoshi(100000) + hops := make([]*route.Hop, 3) + for i := 0; i < 3; i++ { + hopKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + amt := baseAmt - lnwire.MilliSatoshi(uint64(i)*100) + hop := &route.Hop{ + PubKeyBytes: route.NewVertex(hopKey.PubKey()), + ChannelID: uint64(700000 + i), + OutgoingTimeLock: uint32(700000 - i*40), + AmtToForward: amt, + } + if features.customRecords { + hop.CustomRecords = record.CustomSet{ + 65562 + uint64(i): []byte(fmt.Sprintf( + "combo_hop_%d", i, + )), + } + } + if features.hopMetadata { + hop.Metadata = []byte( + fmt.Sprintf("combo_metadata_%d", i), + ) + } + + if i == 2 { + if features.mpp { + var paymentAddr [32]byte + copy( + paymentAddr[:], + []byte("combo_mpp_payment_addr_1234"), + ) + hop.MPP = record.NewMPP( + baseAmt-200, paymentAddr, + ) + } + if features.amp { + var rootShare [32]byte + copy( + rootShare[:], + []byte("combo_amp_root_share_123456"), + ) + var setID [32]byte + copy( + setID[:], + []byte("combo_amp_set_id_12345678"), + ) + hop.AMP = record.NewAMP(rootShare, setID, 0) + } + if features.blindedRoute { + blindingKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + hop.BlindingPoint = blindingKey.PubKey() + hop.EncryptedData = []byte( + "combo_encrypted_blinded_data", + ) + hop.TotalAmtMsat = baseAmt - 200 + } + } + + hops[i] = hop + } + + routeInfo := route.Route{ + TotalTimeLock: 700000, + TotalAmount: baseAmt - 200, + SourcePubKey: sourcePubKey, + Hops: hops, + } + if features.customRecords { + routeInfo.FirstHopWireCustomRecords = lnwire.CustomRecords{ + 65565: []byte("combo_attempt_custom_1"), + 65566: []byte("combo_attempt_custom_2"), + } + } + + *globalAttemptID++ + attemptID := *globalAttemptID + attemptInfo := &HTLCAttemptInfo{ + AttemptID: attemptID, + sessionKey: sessionKeyBytes, + Route: routeInfo, + AttemptTime: creationTime.Add(time.Minute), + Hash: (*lntypes.Hash)(&hash), + } + + attemptKey := make([]byte, len(htlcAttemptInfoKey)+8) + copy(attemptKey, htlcAttemptInfoKey) + byteOrder.PutUint64(attemptKey[len(htlcAttemptInfoKey):], attemptID) + + var ab bytes.Buffer + err = serializeHTLCAttemptInfo(&ab, attemptInfo) + if err != nil { + return err + } + err = htlcBucket.Put(attemptKey, ab.Bytes()) + if err != nil { + return err + } + + settleInfo := &HTLCSettleInfo{ + Preimage: lntypes.Preimage(hash), + SettleTime: creationTime.Add(2 * time.Minute), + } + + settleKey := make([]byte, len(htlcSettleInfoKey)+8) + copy(settleKey, htlcSettleInfoKey) + byteOrder.PutUint64(settleKey[len(htlcSettleInfoKey):], attemptID) + + var sb bytes.Buffer + err = serializeHTLCSettleInfo(&sb, settleInfo) + if err != nil { + return err + } + err = htlcBucket.Put(settleKey, sb.Bytes()) + if err != nil { + return err + } + + var idx bytes.Buffer + err = WriteElements(&idx, paymentIndexTypeHash, hash[:]) + if err != nil { + return err + } + + return indexBucket.Put(seqBytes, idx.Bytes()) +} diff --git a/payments/db/migration1/sql_store.go b/payments/db/migration1/sql_store.go new file mode 100644 index 00000000000..0725bfe59ba --- /dev/null +++ b/payments/db/migration1/sql_store.go @@ -0,0 +1,1972 @@ +package migration1 + +import ( + "bytes" + "context" + "database/sql" + "errors" + "fmt" + "math" + "strconv" + "time" + + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/payments/db/migration1/sqlc" + "github.com/lightningnetwork/lnd/routing/route" + "github.com/lightningnetwork/lnd/sqldb" +) + +// PaymentIntentType represents the type of payment intent. +type PaymentIntentType int16 + +const ( + // PaymentIntentTypeBolt11 indicates a BOLT11 invoice payment. + PaymentIntentTypeBolt11 PaymentIntentType = 0 +) + +// HTLCAttemptResolutionType represents the type of HTLC attempt resolution. +type HTLCAttemptResolutionType int32 + +const ( + // HTLCAttemptResolutionSettled indicates the HTLC attempt was settled + // successfully with a preimage. + HTLCAttemptResolutionSettled HTLCAttemptResolutionType = 1 + + // HTLCAttemptResolutionFailed indicates the HTLC attempt failed. + HTLCAttemptResolutionFailed HTLCAttemptResolutionType = 2 +) + +// SQLQueries is a subset of the sqlc.Querier interface that can be used to +// execute queries against the SQL payments tables. +// +//nolint:ll,interfacebloat +type SQLQueries interface { + /* + Payment DB read operations. + */ + FilterPayments(ctx context.Context, query sqlc.FilterPaymentsParams) ([]sqlc.FilterPaymentsRow, error) + FetchPayment(ctx context.Context, paymentIdentifier []byte) (sqlc.FetchPaymentRow, error) + FetchPaymentsByIDs(ctx context.Context, paymentIDs []int64) ([]sqlc.FetchPaymentsByIDsRow, error) + + CountPayments(ctx context.Context) (int64, error) + + FetchHtlcAttemptsForPayments(ctx context.Context, paymentIDs []int64) ([]sqlc.FetchHtlcAttemptsForPaymentsRow, error) + FetchHtlcAttemptResolutionsForPayments(ctx context.Context, paymentIDs []int64) ([]sqlc.FetchHtlcAttemptResolutionsForPaymentsRow, error) + FetchAllInflightAttempts(ctx context.Context, arg sqlc.FetchAllInflightAttemptsParams) ([]sqlc.PaymentHtlcAttempt, error) + FetchHopsForAttempts(ctx context.Context, htlcAttemptIndices []int64) ([]sqlc.FetchHopsForAttemptsRow, error) + + FetchPaymentDuplicates(ctx context.Context, paymentID int64) ([]sqlc.PaymentDuplicate, error) + + FetchPaymentLevelFirstHopCustomRecords(ctx context.Context, paymentIDs []int64) ([]sqlc.PaymentFirstHopCustomRecord, error) + FetchRouteLevelFirstHopCustomRecords(ctx context.Context, htlcAttemptIndices []int64) ([]sqlc.PaymentAttemptFirstHopCustomRecord, error) + FetchHopLevelCustomRecords(ctx context.Context, hopIDs []int64) ([]sqlc.PaymentHopCustomRecord, error) + + /* + Payment DB write operations. + */ + InsertPaymentIntent(ctx context.Context, arg sqlc.InsertPaymentIntentParams) (int64, error) + InsertPayment(ctx context.Context, arg sqlc.InsertPaymentParams) (int64, error) + InsertPaymentFirstHopCustomRecord(ctx context.Context, arg sqlc.InsertPaymentFirstHopCustomRecordParams) error + + InsertHtlcAttempt(ctx context.Context, arg sqlc.InsertHtlcAttemptParams) (int64, error) + InsertRouteHop(ctx context.Context, arg sqlc.InsertRouteHopParams) (int64, error) + InsertRouteHopMpp(ctx context.Context, arg sqlc.InsertRouteHopMppParams) error + InsertRouteHopAmp(ctx context.Context, arg sqlc.InsertRouteHopAmpParams) error + InsertRouteHopBlinded(ctx context.Context, arg sqlc.InsertRouteHopBlindedParams) error + + InsertPaymentAttemptFirstHopCustomRecord(ctx context.Context, arg sqlc.InsertPaymentAttemptFirstHopCustomRecordParams) error + InsertPaymentHopCustomRecord(ctx context.Context, arg sqlc.InsertPaymentHopCustomRecordParams) error + + SettleAttempt(ctx context.Context, arg sqlc.SettleAttemptParams) error + FailAttempt(ctx context.Context, arg sqlc.FailAttemptParams) error + + FailPayment(ctx context.Context, arg sqlc.FailPaymentParams) (sql.Result, error) + + DeletePayment(ctx context.Context, paymentID int64) error + + // DeleteFailedAttempts removes all failed HTLCs from the db for a + // given payment. + DeleteFailedAttempts(ctx context.Context, paymentID int64) error + + /* + Migration specific queries. + + These queries are used ONLY for the one-time migration from KV + to SQL. + */ + + // InsertPaymentMig is a migration-only variant of InsertPayment that + // allows setting fail_reason when inserting historical payments. + InsertPaymentMig(ctx context.Context, arg sqlc.InsertPaymentMigParams) (int64, error) + + // InsertPaymentDuplicateMig inserts a duplicate payment record during + // migration. + InsertPaymentDuplicateMig(ctx context.Context, arg sqlc.InsertPaymentDuplicateMigParams) (int64, error) +} + +// BatchedSQLQueries is a version of the SQLQueries that's capable +// of batched database operations. +type BatchedSQLQueries interface { + SQLQueries + sqldb.BatchedTx[SQLQueries] +} + +// SQLStore represents a storage backend. +type SQLStore struct { + cfg *SQLStoreConfig + db BatchedSQLQueries +} + +// A compile-time constraint to ensure SQLStore implements DB. +var _ DB = (*SQLStore)(nil) + +// SQLStoreConfig holds the configuration for the SQLStore. +type SQLStoreConfig struct { + // QueryConfig holds configuration values for SQL queries. + QueryCfg *sqldb.QueryConfig +} + +// NewSQLStore creates a new SQLStore instance given an open +// BatchedSQLPaymentsQueries storage backend. +func NewSQLStore(cfg *SQLStoreConfig, db BatchedSQLQueries, + options ...OptionModifier) (*SQLStore, error) { + + opts := DefaultOptions() + for _, applyOption := range options { + applyOption(opts) + } + + if opts.NoMigration { + return nil, fmt.Errorf("the NoMigration option is not yet " + + "supported for SQL stores") + } + + return &SQLStore{ + cfg: cfg, + db: db, + }, nil +} + +// A compile-time constraint to ensure SQLStore implements DB. +var _ DB = (*SQLStore)(nil) + +// fetchPaymentWithCompleteData fetches a payment with all its related data +// including attempts, hops, and custom records from the database. +// This is a convenience wrapper around the batch loading functions for single +// payment operations. +func fetchPaymentWithCompleteData(ctx context.Context, + cfg *sqldb.QueryConfig, db SQLQueries, + dbPayment sqlc.PaymentAndIntent) (*MPPayment, error) { + + payment := dbPayment.GetPayment() + + // Load batch data for this single payment. + batchData, err := batchLoadPaymentDetailsData( + ctx, cfg, db, []int64{payment.ID}, + ) + if err != nil { + return nil, fmt.Errorf("failed to load batch data: %w", err) + } + + // Build the payment from the batch data. + return buildPaymentFromBatchData(dbPayment, batchData) +} + +// paymentsCompleteData holds the full payment data when batch loading base +// payment data and all the related data for a payment. +type paymentsCompleteData struct { + *paymentsBaseData + *paymentsDetailsData +} + +// batchLoadPayments loads the full payment data for a batch of payment IDs. +func batchLoadPayments(ctx context.Context, cfg *sqldb.QueryConfig, + db SQLQueries, paymentIDs []int64) (*paymentsCompleteData, error) { + + baseData, err := batchLoadpaymentsBaseData(ctx, cfg, db, paymentIDs) + if err != nil { + return nil, fmt.Errorf("failed to load payment base data: %w", + err) + } + + batchData, err := batchLoadPaymentDetailsData(ctx, cfg, db, paymentIDs) + if err != nil { + return nil, fmt.Errorf("failed to load payment batch data: %w", + err) + } + + return &paymentsCompleteData{ + paymentsBaseData: baseData, + paymentsDetailsData: batchData, + }, nil +} + +// paymentsBaseData holds the base payment and intent data for a batch of +// payments. +type paymentsBaseData struct { + // paymentsAndIntents maps payment ID to its payment and intent data. + paymentsAndIntents map[int64]sqlc.PaymentAndIntent +} + +// batchLoadpaymentsBaseData loads the base payment and payment intent data for +// a batch of payment IDs. This complements loadPaymentsBatchData which loads +// related data (attempts, hops, custom records) but not the payment table +// and payment intent table data. +func batchLoadpaymentsBaseData(ctx context.Context, + cfg *sqldb.QueryConfig, db SQLQueries, + paymentIDs []int64) (*paymentsBaseData, error) { + + baseData := &paymentsBaseData{ + paymentsAndIntents: make(map[int64]sqlc.PaymentAndIntent), + } + + if len(paymentIDs) == 0 { + return baseData, nil + } + + err := sqldb.ExecuteBatchQuery( + ctx, cfg, paymentIDs, + func(id int64) int64 { return id }, + func(ctx context.Context, ids []int64) ( + []sqlc.FetchPaymentsByIDsRow, error) { + + records, err := db.FetchPaymentsByIDs( + ctx, ids, + ) + + return records, err + }, + func(ctx context.Context, + payment sqlc.FetchPaymentsByIDsRow) error { + + baseData.paymentsAndIntents[payment.ID] = payment + + return nil + }, + ) + if err != nil { + return nil, fmt.Errorf("failed to fetch payment base "+ + "data: %w", err) + } + + return baseData, nil +} + +// paymentsRelatedData holds all the batch-loaded data for multiple payments. +// This does not include the base payment and intent data which is fetched +// separately. It includes the additional data like attempts, hops, hop custom +// records, and route custom records. +type paymentsDetailsData struct { + // paymentCustomRecords maps payment ID to its custom records. + paymentCustomRecords map[int64][]sqlc.PaymentFirstHopCustomRecord + + // attempts maps payment ID to its HTLC attempts. + attempts map[int64][]sqlc.FetchHtlcAttemptsForPaymentsRow + + // hopsByAttempt maps attempt index to its hops. + hopsByAttempt map[int64][]sqlc.FetchHopsForAttemptsRow + + // hopCustomRecords maps hop ID to its custom records. + hopCustomRecords map[int64][]sqlc.PaymentHopCustomRecord + + // routeCustomRecords maps attempt index to its route-level custom + // records. + routeCustomRecords map[int64][]sqlc.PaymentAttemptFirstHopCustomRecord +} + +// batchLoadPaymentCustomRecords loads payment-level custom records for a given +// set of payment IDs. It uses a batch query to fetch all custom records for +// the given payment IDs. +func batchLoadPaymentCustomRecords(ctx context.Context, + cfg *sqldb.QueryConfig, db SQLQueries, paymentIDs []int64, + batchData *paymentsDetailsData) error { + + return sqldb.ExecuteBatchQuery( + ctx, cfg, paymentIDs, + func(id int64) int64 { return id }, + func(ctx context.Context, ids []int64) ( + []sqlc.PaymentFirstHopCustomRecord, error) { + + //nolint:ll + records, err := db.FetchPaymentLevelFirstHopCustomRecords( + ctx, ids, + ) + + return records, err + }, + func(ctx context.Context, + record sqlc.PaymentFirstHopCustomRecord) error { + + paymentRecords := + batchData.paymentCustomRecords[record.PaymentID] + + batchData.paymentCustomRecords[record.PaymentID] = + append(paymentRecords, record) + + return nil + }, + ) +} + +// batchLoadHtlcAttempts loads HTLC attempts for all payments and returns all +// attempt indices. It uses a batch query to fetch all attempts for the given +// payment IDs. +func batchLoadHtlcAttempts(ctx context.Context, cfg *sqldb.QueryConfig, + db SQLQueries, paymentIDs []int64, + batchData *paymentsDetailsData) ([]int64, error) { + + var allAttemptIndices []int64 + + err := sqldb.ExecuteBatchQuery( + ctx, cfg, paymentIDs, + func(id int64) int64 { return id }, + func(ctx context.Context, ids []int64) ( + []sqlc.FetchHtlcAttemptsForPaymentsRow, error) { + + return db.FetchHtlcAttemptsForPayments(ctx, ids) + }, + func(ctx context.Context, + attempt sqlc.FetchHtlcAttemptsForPaymentsRow) error { + + batchData.attempts[attempt.PaymentID] = append( + batchData.attempts[attempt.PaymentID], attempt, + ) + allAttemptIndices = append( + allAttemptIndices, attempt.AttemptIndex, + ) + + return nil + }, + ) + + return allAttemptIndices, err +} + +// batchLoadHopsForAttempts loads hops for all attempts and returns all hop IDs. +// It uses a batch query to fetch all hops for the given attempt indices. +func batchLoadHopsForAttempts(ctx context.Context, cfg *sqldb.QueryConfig, + db SQLQueries, attemptIndices []int64, + batchData *paymentsDetailsData) ([]int64, error) { + + var hopIDs []int64 + + err := sqldb.ExecuteBatchQuery( + ctx, cfg, attemptIndices, + func(idx int64) int64 { return idx }, + func(ctx context.Context, indices []int64) ( + []sqlc.FetchHopsForAttemptsRow, error) { + + return db.FetchHopsForAttempts(ctx, indices) + }, + func(ctx context.Context, + hop sqlc.FetchHopsForAttemptsRow) error { + + attemptHops := + batchData.hopsByAttempt[hop.HtlcAttemptIndex] + + batchData.hopsByAttempt[hop.HtlcAttemptIndex] = + append(attemptHops, hop) + + hopIDs = append(hopIDs, hop.ID) + + return nil + }, + ) + + return hopIDs, err +} + +// batchLoadHopCustomRecords loads hop-level custom records for all hops. It +// uses a batch query to fetch all custom records for the given hop IDs. +func batchLoadHopCustomRecords(ctx context.Context, cfg *sqldb.QueryConfig, + db SQLQueries, hopIDs []int64, batchData *paymentsDetailsData) error { + + return sqldb.ExecuteBatchQuery( + ctx, cfg, hopIDs, + func(id int64) int64 { return id }, + func(ctx context.Context, ids []int64) ( + []sqlc.PaymentHopCustomRecord, error) { + + return db.FetchHopLevelCustomRecords(ctx, ids) + }, + func(ctx context.Context, + record sqlc.PaymentHopCustomRecord) error { + + // TODO(ziggie): Can we get rid of this? + // This has to be in place otherwise the + // comparison will not match. + if record.Value == nil { + record.Value = []byte{} + } + + batchData.hopCustomRecords[record.HopID] = append( + batchData.hopCustomRecords[record.HopID], + record, + ) + + return nil + }, + ) +} + +// batchLoadRouteCustomRecords loads route-level first hop custom records for +// all attempts. It uses a batch query to fetch all custom records for the given +// attempt indices. +func batchLoadRouteCustomRecords(ctx context.Context, cfg *sqldb.QueryConfig, + db SQLQueries, attemptIndices []int64, + batchData *paymentsDetailsData) error { + + return sqldb.ExecuteBatchQuery( + ctx, cfg, attemptIndices, + func(idx int64) int64 { return idx }, + func(ctx context.Context, indices []int64) ( + []sqlc.PaymentAttemptFirstHopCustomRecord, error) { + + return db.FetchRouteLevelFirstHopCustomRecords( + ctx, indices, + ) + }, + func(ctx context.Context, + record sqlc.PaymentAttemptFirstHopCustomRecord) error { + + idx := record.HtlcAttemptIndex + attemptRecords := batchData.routeCustomRecords[idx] + + batchData.routeCustomRecords[idx] = + append(attemptRecords, record) + + return nil + }, + ) +} + +// paymentStatusData holds lightweight resolution data for computing +// payment status efficiently during deletion operations. +type paymentStatusData struct { + // resolutionTypes maps payment ID to a list of resolution types + // for that payment's HTLC attempts. + resolutionTypes map[int64][]sql.NullInt32 +} + +// batchLoadPaymentResolutions loads only HTLC resolution types for multiple +// payments. This is a lightweight alternative to batchLoadPaymentsRelatedData +// that's optimized for operations that only need to determine payment status. +func batchLoadPaymentResolutions(ctx context.Context, cfg *sqldb.QueryConfig, + db SQLQueries, paymentIDs []int64) (*paymentStatusData, error) { + + batchStatusData := &paymentStatusData{ + resolutionTypes: make(map[int64][]sql.NullInt32), + } + + if len(paymentIDs) == 0 { + return batchStatusData, nil + } + + // Use a batch query to fetch all resolution types for the given payment + // IDs. + err := sqldb.ExecuteBatchQuery( + ctx, cfg, paymentIDs, + func(id int64) int64 { return id }, + func(ctx context.Context, ids []int64) ( + []sqlc.FetchHtlcAttemptResolutionsForPaymentsRow, + error) { + + return db.FetchHtlcAttemptResolutionsForPayments( + ctx, ids, + ) + }, + //nolint:ll + func(ctx context.Context, + res sqlc.FetchHtlcAttemptResolutionsForPaymentsRow) error { + + // Group resolutions by payment ID. + batchStatusData.resolutionTypes[res.PaymentID] = append( + batchStatusData.resolutionTypes[res.PaymentID], + res.ResolutionType, + ) + + return nil + }, + ) + if err != nil { + return nil, fmt.Errorf("failed to fetch HTLC resolutions: %w", + err) + } + + return batchStatusData, nil +} + +// loadPaymentResolutions is a single-payment wrapper around +// batchLoadPaymentResolutions for convenience and to prevent duplicate queries +// so we reuse the same batch query for all payments. +func loadPaymentResolutions(ctx context.Context, cfg *sqldb.QueryConfig, + db SQLQueries, paymentID int64) ([]sql.NullInt32, error) { + + batchData, err := batchLoadPaymentResolutions( + ctx, cfg, db, []int64{paymentID}, + ) + if err != nil { + return nil, err + } + + return batchData.resolutionTypes[paymentID], nil +} + +// computePaymentStatusFromResolutions determines the payment status from +// resolution types and failure reason without building the complete MPPayment +// structure. This is a lightweight version that builds minimal HTLCAttempt +// structures and delegates to decidePaymentStatus for consistency. +func computePaymentStatusFromResolutions(resolutionTypes []sql.NullInt32, + failReason sql.NullInt32) (PaymentStatus, error) { + + // Build minimal HTLCAttempt slice with only resolution info. + htlcs := make([]HTLCAttempt, len(resolutionTypes)) + for i, resType := range resolutionTypes { + if !resType.Valid { + // NULL resolution_type means in-flight (no Settle, no + // Failure). + continue + } + + switch HTLCAttemptResolutionType(resType.Int32) { + case HTLCAttemptResolutionSettled: + // Mark as settled (preimage details not needed for + // status). + htlcs[i].Settle = &HTLCSettleInfo{} + + case HTLCAttemptResolutionFailed: + // Mark as failed (failure details not needed for + // status). + htlcs[i].Failure = &HTLCFailInfo{} + + default: + return 0, fmt.Errorf("unknown resolution type: %v", + resType.Int32) + } + } + + // Convert fail reason to FailureReason pointer. + var failureReason *FailureReason + if failReason.Valid { + reason := FailureReason(failReason.Int32) + failureReason = &reason + } + + // Use the existing status decision logic. + return decidePaymentStatus(htlcs, failureReason) +} + +// batchLoadPaymentDetailsData loads all related data for multiple payments in +// batch. It uses a batch queries to fetch all data for the given payment IDs. +func batchLoadPaymentDetailsData(ctx context.Context, cfg *sqldb.QueryConfig, + db SQLQueries, paymentIDs []int64) (*paymentsDetailsData, error) { + + batchData := &paymentsDetailsData{ + paymentCustomRecords: make( + map[int64][]sqlc.PaymentFirstHopCustomRecord, + ), + attempts: make( + map[int64][]sqlc.FetchHtlcAttemptsForPaymentsRow, + ), + hopsByAttempt: make( + map[int64][]sqlc.FetchHopsForAttemptsRow, + ), + hopCustomRecords: make( + map[int64][]sqlc.PaymentHopCustomRecord, + ), + routeCustomRecords: make( + map[int64][]sqlc.PaymentAttemptFirstHopCustomRecord, + ), + } + + if len(paymentIDs) == 0 { + return batchData, nil + } + + // Load payment-level custom records. + err := batchLoadPaymentCustomRecords( + ctx, cfg, db, paymentIDs, batchData, + ) + if err != nil { + return nil, fmt.Errorf("failed to fetch payment custom "+ + "records: %w", err) + } + + // Load HTLC attempts and collect attempt indices. + allAttemptIndices, err := batchLoadHtlcAttempts( + ctx, cfg, db, paymentIDs, batchData, + ) + if err != nil { + return nil, fmt.Errorf("failed to fetch HTLC attempts: %w", + err) + } + + if len(allAttemptIndices) == 0 { + // No attempts, return early. + return batchData, nil + } + + // Load hops for all attempts and collect hop IDs. + hopIDs, err := batchLoadHopsForAttempts( + ctx, cfg, db, allAttemptIndices, batchData, + ) + if err != nil { + return nil, fmt.Errorf("failed to fetch hops for attempts: %w", + err) + } + + // Load hop-level custom records if there are any hops. + if len(hopIDs) > 0 { + err = batchLoadHopCustomRecords(ctx, cfg, db, hopIDs, batchData) + if err != nil { + return nil, fmt.Errorf("failed to fetch hop custom "+ + "records: %w", err) + } + } + + // Load route-level first hop custom records. + err = batchLoadRouteCustomRecords( + ctx, cfg, db, allAttemptIndices, batchData, + ) + if err != nil { + return nil, fmt.Errorf("failed to fetch route custom "+ + "records: %w", err) + } + + return batchData, nil +} + +// buildPaymentFromBatchData builds a complete MPPayment from a database payment +// and pre-loaded batch data. +func buildPaymentFromBatchData(dbPayment sqlc.PaymentAndIntent, + batchData *paymentsDetailsData) (*MPPayment, error) { + + // The query will only return BOLT 11 payment intents or intents with + // no intent type set. + paymentIntent := dbPayment.GetPaymentIntent() + paymentRequest := paymentIntent.IntentPayload + + payment := dbPayment.GetPayment() + + // Get payment-level custom records from batch data. + customRecords := batchData.paymentCustomRecords[payment.ID] + + // Convert to the FirstHopCustomRecords map. + var firstHopCustomRecords lnwire.CustomRecords + if len(customRecords) > 0 { + firstHopCustomRecords = make(lnwire.CustomRecords) + for _, record := range customRecords { + firstHopCustomRecords[uint64(record.Key)] = record.Value + } + } + + // Convert database payment data to the PaymentCreationInfo struct. + info := dbPaymentToCreationInfo( + payment.PaymentIdentifier, payment.AmountMsat, + payment.CreatedAt, paymentRequest, firstHopCustomRecords, + ) + + // Get all HTLC attempts from batch data for a given payment. + dbAttempts := batchData.attempts[payment.ID] + + // Convert all attempts to HTLCAttempt structs using the pre-loaded + // batch data. + attempts := make([]HTLCAttempt, 0, len(dbAttempts)) + for _, dbAttempt := range dbAttempts { + attemptIndex := dbAttempt.AttemptIndex + // Convert the batch row type to the single row type. + attempt, err := dbAttemptToHTLCAttempt( + dbAttempt, batchData.hopsByAttempt[attemptIndex], + batchData.hopCustomRecords, + batchData.routeCustomRecords[attemptIndex], + ) + if err != nil { + return nil, fmt.Errorf("failed to convert attempt "+ + "%d: %w", attemptIndex, err) + } + attempts = append(attempts, *attempt) + } + + // Set the failure reason if present. + // + // TODO(ziggie): Rename it to Payment Memo in the database? + var failureReason *FailureReason + if payment.FailReason.Valid { + reason := FailureReason(payment.FailReason.Int32) + failureReason = &reason + } + + mpPayment := &MPPayment{ + SequenceNum: uint64(payment.ID), + Info: info, + HTLCs: attempts, + FailureReason: failureReason, + } + + // The status and state will be determined by calling + // SetState after construction. + if err := mpPayment.SetState(); err != nil { + return nil, fmt.Errorf("failed to set payment state: %w", err) + } + + return mpPayment, nil +} + +// QueryPayments queries and retrieves payments from the database with support +// for filtering, pagination, and efficient batch loading of related data. +// +// The function accepts a Query parameter that controls: +// - Pagination: IndexOffset specifies where to start (exclusive), and +// MaxPayments limits the number of results returned +// - Ordering: Reversed flag determines if results are returned in reverse +// chronological order +// - Filtering: CreationDateStart/End filter by creation time, and +// IncludeIncomplete controls whether non-succeeded payments are included +// - Metadata: CountTotal flag determines if the total payment count should +// be calculated +// +// The function optimizes performance by loading all related data (HTLCs, +// sequences, failure reasons, etc.) for multiple payments in a single batch +// query, rather than fetching each payment's data individually. +// +// Returns a Response containing: +// - Payments: the list of matching payments with complete data +// - FirstIndexOffset/LastIndexOffset: pagination cursors for the first and +// last payment in the result set +// - TotalCount: total number of payments in the database (if CountTotal was +// requested, otherwise 0) +// +// This is part of the DB interface. +func (s *SQLStore) QueryPayments(ctx context.Context, query Query) (Response, + error) { + + if query.MaxPayments == 0 { + return Response{}, fmt.Errorf("max payments must be non-zero") + } + + var ( + allPayments []*MPPayment + totalCount int64 + initialCursor int64 + ) + + extractCursor := func(row sqlc.FilterPaymentsRow) int64 { + return row.Payment.ID + } + + err := s.db.ExecTx(ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error { + // We first count all payments to determine the total count + // if requested. + if query.CountTotal { + totalPayments, err := db.CountPayments(ctx) + if err != nil { + return fmt.Errorf("failed to count "+ + "payments: %w", err) + } + totalCount = totalPayments + } + + // collectFunc extracts the payment ID from each payment row. + collectFunc := func(row sqlc.FilterPaymentsRow) (int64, error) { + return row.Payment.ID, nil + } + + // batchDataFunc loads all related data for a batch of payments. + batchDataFunc := func(ctx context.Context, paymentIDs []int64) ( + *paymentsDetailsData, error) { + + return batchLoadPaymentDetailsData( + ctx, s.cfg.QueryCfg, db, paymentIDs, + ) + } + + // processPayment processes each payment with the batch-loaded + // data. + processPayment := func(ctx context.Context, + dbPayment sqlc.FilterPaymentsRow, + batchData *paymentsDetailsData) error { + + // Build the payment from the pre-loaded batch data. + mpPayment, err := buildPaymentFromBatchData( + dbPayment, batchData, + ) + if err != nil { + return fmt.Errorf("failed to fetch payment "+ + "with complete data: %w", err) + } + + // To keep compatibility with the old API, we only + // return non-succeeded payments if requested. + if mpPayment.Status != StatusSucceeded && + !query.IncludeIncomplete { + + return nil + } + + if uint64(len(allPayments)) >= query.MaxPayments { + return errMaxPaymentsReached + } + + allPayments = append(allPayments, mpPayment) + + return nil + } + + queryFunc := func(ctx context.Context, lastID int64, + limit int32) ([]sqlc.FilterPaymentsRow, error) { + + filterParams := sqlc.FilterPaymentsParams{ + NumLimit: limit, + Reverse: query.Reversed, + // For now there only BOLT 11 payment intents + // exist. + IntentType: sqldb.SQLInt16( + PaymentIntentTypeBolt11, + ), + } + + if query.Reversed { + filterParams.IndexOffsetLet = sqldb.SQLInt64( + lastID, + ) + } else { + filterParams.IndexOffsetGet = sqldb.SQLInt64( + lastID, + ) + } + + // Add potential date filters if specified. + if query.CreationDateStart != 0 { + filterParams.CreatedAfter = sqldb.SQLTime( + time.Unix(query.CreationDateStart, 0). + UTC(), + ) + } + if query.CreationDateEnd != 0 { + filterParams.CreatedBefore = sqldb.SQLTime( + time.Unix(query.CreationDateEnd, 0). + UTC(), + ) + } + + return db.FilterPayments(ctx, filterParams) + } + + if query.Reversed { + if query.IndexOffset == 0 { + initialCursor = int64(math.MaxInt64) + } else { + initialCursor = int64(query.IndexOffset) + } + } else { + initialCursor = int64(query.IndexOffset) + } + + return sqldb.ExecuteCollectAndBatchWithSharedDataQuery( + ctx, s.cfg.QueryCfg, initialCursor, queryFunc, + extractCursor, collectFunc, batchDataFunc, + processPayment, + ) + }, func() { + allPayments = nil + }) + + // We make sure we don't return an error if we reached the maximum + // number of payments. Which is the pagination limit for the query + // itself. + if err != nil && !errors.Is(err, errMaxPaymentsReached) { + return Response{}, fmt.Errorf("failed to query payments: %w", + err) + } + + // Handle case where no payments were found + if len(allPayments) == 0 { + return Response{ + Payments: allPayments, + FirstIndexOffset: 0, + LastIndexOffset: 0, + TotalCount: uint64(totalCount), + }, nil + } + + // If the query was reversed, we need to reverse the payment list + // to match the kvstore behavior and return payments in forward order. + if query.Reversed { + for i, j := 0, len(allPayments)-1; i < j; i, j = i+1, j-1 { + allPayments[i], allPayments[j] = allPayments[j], + allPayments[i] + } + } + + return Response{ + Payments: allPayments, + FirstIndexOffset: allPayments[0].SequenceNum, + LastIndexOffset: allPayments[len(allPayments)-1].SequenceNum, + TotalCount: uint64(totalCount), + }, nil +} + +// fetchPaymentByHash fetches a payment by its hash from the database. It is a +// convenience wrapper around the FetchPayment method and checks for +// no rows error and returns ErrPaymentNotInitiated if no payment is found. +func fetchPaymentByHash(ctx context.Context, db SQLQueries, + paymentHash lntypes.Hash) (sqlc.FetchPaymentRow, error) { + + dbPayment, err := db.FetchPayment(ctx, paymentHash[:]) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return dbPayment, fmt.Errorf("failed to fetch payment: %w", err) + } + + if errors.Is(err, sql.ErrNoRows) { + return dbPayment, ErrPaymentNotInitiated + } + + return dbPayment, nil +} + +// FetchPayment retrieves a complete payment record from the database by its +// payment hash. The returned MPPayment includes all payment metadata such as +// creation info, payment status, current state, all HTLC attempts (both +// successful and failed), and the failure reason if the payment has been +// marked as failed. +// +// Returns ErrPaymentNotInitiated if no payment with the given hash exists. +// +// This is part of the DB interface. +func (s *SQLStore) FetchPayment(ctx context.Context, + paymentHash lntypes.Hash) (*MPPayment, error) { + + var mpPayment *MPPayment + + err := s.db.ExecTx(ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error { + dbPayment, err := fetchPaymentByHash(ctx, db, paymentHash) + if err != nil { + return err + } + + mpPayment, err = fetchPaymentWithCompleteData( + ctx, s.cfg.QueryCfg, db, dbPayment, + ) + if err != nil { + return fmt.Errorf("failed to fetch payment with "+ + "complete data: %w", err) + } + + return nil + }, sqldb.NoOpReset) + if err != nil { + return nil, err + } + + return mpPayment, nil +} + +// FetchInFlightPayments retrieves all payments that have HTLC attempts +// currently in flight (not yet settled or failed). These are payments with at +// least one HTLC attempt that has been registered but has no resolution record. +// +// The SQLStore implementation provides a significant performance improvement +// over the KVStore implementation by using targeted SQL queries instead of +// scanning all payments. +// +// This method is part of the PaymentReader interface, which is embedded in the +// DB interface. It's typically called during node startup to resume monitoring +// of pending payments and ensure HTLCs are properly tracked. +// +// TODO(ziggie): Consider changing the interface to use a callback or iterator +// pattern instead of returning all payments at once. This would allow +// processing payments one at a time without holding them all in memory +// simultaneously: +// - Callback: func FetchInFlightPayments(ctx, func(*MPPayment) error) error +// - Iterator: func FetchInFlightPayments(ctx) (PaymentIterator, error) +// +// While inflight payments are typically a small subset, this would improve +// memory efficiency for nodes with unusually high numbers of concurrent +// payments and would better leverage the existing pagination infrastructure. +func (s *SQLStore) FetchInFlightPayments(ctx context.Context) ([]*MPPayment, + error) { + + var mpPayments []*MPPayment + + err := s.db.ExecTx(ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error { + // Track which payment IDs we've already processed across all + // pages to avoid loading the same payment multiple times when + // multiple inflight attempts belong to the same payment. + processedPayments := make(map[int64]*MPPayment) + + extractCursor := func(row sqlc.PaymentHtlcAttempt) int64 { + return row.AttemptIndex + } + + // collectFunc extracts the payment ID from each attempt row. + collectFunc := func(row sqlc.PaymentHtlcAttempt) ( + int64, error) { + + return row.PaymentID, nil + } + + // batchDataFunc loads payment data for a batch of payment IDs, + // but only for IDs we haven't processed yet. + batchDataFunc := func(ctx context.Context, + paymentIDs []int64) (*paymentsCompleteData, error) { + + // Filter out already-processed payment IDs. + uniqueIDs := make([]int64, 0, len(paymentIDs)) + for _, id := range paymentIDs { + _, processed := processedPayments[id] + if !processed { + uniqueIDs = append(uniqueIDs, id) + } + } + + // If uniqueIDs is empty, the batch load will return + // empty batch data. + return batchLoadPayments( + ctx, s.cfg.QueryCfg, db, uniqueIDs, + ) + } + + // processAttempt processes each attempt. We only build and + // store the payment once per unique payment ID. + processAttempt := func(ctx context.Context, + row sqlc.PaymentHtlcAttempt, + batchData *paymentsCompleteData) error { + + // Skip if we've already processed this payment. + _, processed := processedPayments[row.PaymentID] + if processed { + return nil + } + + dbPayment := batchData.paymentsAndIntents[row.PaymentID] + + // Build the payment from batch data. + mpPayment, err := buildPaymentFromBatchData( + dbPayment, batchData.paymentsDetailsData, + ) + if err != nil { + return fmt.Errorf("failed to build payment: %w", + err) + } + + // Store in our processed map. + processedPayments[row.PaymentID] = mpPayment + + return nil + } + + queryFunc := func(ctx context.Context, lastAttemptIndex int64, + limit int32) ([]sqlc.PaymentHtlcAttempt, + error) { + + return db.FetchAllInflightAttempts(ctx, + sqlc.FetchAllInflightAttemptsParams{ + AttemptIndex: lastAttemptIndex, + Limit: limit, + }, + ) + } + + err := sqldb.ExecuteCollectAndBatchWithSharedDataQuery( + ctx, s.cfg.QueryCfg, int64(-1), queryFunc, + extractCursor, collectFunc, batchDataFunc, + processAttempt, + ) + if err != nil { + return err + } + + // Convert map to slice. + mpPayments = make([]*MPPayment, 0, len(processedPayments)) + for _, payment := range processedPayments { + mpPayments = append(mpPayments, payment) + } + + return nil + }, func() { + mpPayments = nil + }) + if err != nil { + return nil, fmt.Errorf("failed to fetch inflight "+ + "payments: %w", err) + } + + return mpPayments, nil +} + +// DeleteFailedAttempts removes all failed HTLC attempts from the database for +// the specified payment, while preserving the payment record itself and any +// successful or in-flight attempts. +// +// The method performs the following validations before deletion: +// - StatusInitiated: Can delete failed attempts +// - StatusInFlight: Cannot delete, returns ErrPaymentInFlight (active HTLCs +// still on the network) +// - StatusSucceeded: Can delete failed attempts (payment completed) +// - StatusFailed: Can delete failed attempts (payment permanently failed) +// +// This method is idempotent - calling it multiple times on the same payment +// has no adverse effects. +// +// This method is part of the PaymentControl interface, which is embedded in +// the PaymentWriter interface and ultimately the DB interface. It represents +// the final step (step 5) in the payment lifecycle control flow and should be +// called after a payment reaches a terminal state (succeeded or permanently +// failed) to clean up historical failed attempts. +func (s *SQLStore) DeleteFailedAttempts(ctx context.Context, + paymentHash lntypes.Hash) error { + + err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error { + dbPayment, err := fetchPaymentByHash(ctx, db, paymentHash) + if err != nil { + return err + } + + paymentStatus, err := computePaymentStatusFromDB( + ctx, s.cfg.QueryCfg, db, dbPayment, + ) + if err != nil { + return fmt.Errorf("failed to compute payment "+ + "status: %w", err) + } + + if err := paymentStatus.removable(); err != nil { + return fmt.Errorf("cannot delete failed "+ + "attempts for payment %v: %w", paymentHash, err) + } + + // Then we delete the failed attempts for this payment. + return db.DeleteFailedAttempts(ctx, dbPayment.GetPayment().ID) + }, sqldb.NoOpReset) + if err != nil { + return fmt.Errorf("failed to delete failed attempts for "+ + "payment %v: %w", paymentHash, err) + } + + return nil +} + +// computePaymentStatusFromDB computes the payment status by fetching minimal +// data from the database. This is a lightweight query optimized for SQL that +// doesn't load route data, making it significantly more efficient than +// FetchPayment when only the status is needed. +func computePaymentStatusFromDB(ctx context.Context, cfg *sqldb.QueryConfig, + db SQLQueries, dbPayment sqlc.PaymentAndIntent) (PaymentStatus, error) { + + payment := dbPayment.GetPayment() + + // Load the resolution types for the payment. + resolutionTypes, err := loadPaymentResolutions( + ctx, cfg, db, payment.ID, + ) + if err != nil { + return 0, fmt.Errorf("failed to load payment resolutions: %w", + err) + } + + // Use the lightweight status computation. + status, err := computePaymentStatusFromResolutions( + resolutionTypes, payment.FailReason, + ) + if err != nil { + return 0, fmt.Errorf("failed to compute payment status: %w", + err) + } + + return status, nil +} + +// DeletePayment removes a payment or its failed HTLC attempts from the +// database based on the failedAttemptsOnly flag. +// +// If failedAttemptsOnly is true, this method deletes only the failed HTLC +// attempts for the payment while preserving the payment record itself and any +// successful or in-flight attempts. This is useful for cleaning up historical +// failed attempts after a payment reaches a terminal state. +// +// If failedAttemptsOnly is false, this method deletes the entire payment +// record including all payment metadata, payment creation info, all HTLC +// attempts (both failed and successful), and associated data such as payment +// intents and custom records. +// +// Before deletion, this method validates the payment status to ensure it's +// safe to delete: +// - StatusInitiated: Can be deleted (no HTLCs sent yet) +// - StatusInFlight: Cannot be deleted, returns ErrPaymentInFlight (active +// HTLCs on the network) +// - StatusSucceeded: Can be deleted (payment completed successfully) +// - StatusFailed: Can be deleted (payment has failed permanently) +// +// Returns an error if the payment has in-flight HTLCs or if the payment +// doesn't exist. +// +// This method is part of the PaymentWriter interface, which is embedded in +// the DB interface. +func (s *SQLStore) DeletePayment(ctx context.Context, paymentHash lntypes.Hash, + failedHtlcsOnly bool) error { + + err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error { + dbPayment, err := fetchPaymentByHash(ctx, db, paymentHash) + if err != nil { + return err + } + + paymentStatus, err := computePaymentStatusFromDB( + ctx, s.cfg.QueryCfg, db, dbPayment, + ) + if err != nil { + return fmt.Errorf("failed to compute payment "+ + "status: %w", err) + } + + if err := paymentStatus.removable(); err != nil { + return fmt.Errorf("payment %v cannot be deleted: %w", + paymentHash, err) + } + + // If we are only deleting failed HTLCs, we delete them. + if failedHtlcsOnly { + return db.DeleteFailedAttempts( + ctx, dbPayment.GetPayment().ID, + ) + } + + // In case we are not deleting failed HTLCs, we delete the + // payment which will cascade delete all related data. + return db.DeletePayment(ctx, dbPayment.GetPayment().ID) + }, sqldb.NoOpReset) + if err != nil { + return fmt.Errorf("failed to delete failed attempts for "+ + "payment %v: %w", paymentHash, err) + } + + return nil +} + +// InitPayment creates a new payment record in the database with the given +// payment hash and creation info. +// +// Before creating the payment, this method checks if a payment with the same +// hash already exists and validates whether initialization is allowed based on +// the existing payment's status: +// - StatusInitiated: Returns ErrPaymentExists (payment already created, +// HTLCs may be in flight) +// - StatusInFlight: Returns ErrPaymentInFlight (payment currently being +// attempted) +// - StatusSucceeded: Returns ErrAlreadyPaid (payment already succeeded) +// - StatusFailed: Allows retry by deleting the old payment record and +// creating a new one +// +// If no existing payment is found, a new payment record is created with +// StatusInitiated and stored with all associated metadata. +// +// This method is part of the PaymentControl interface, which is embedded in +// the PaymentWriter interface and ultimately the DB interface, representing +// the first step in the payment lifecycle control flow. +func (s *SQLStore) InitPayment(ctx context.Context, paymentHash lntypes.Hash, + paymentCreationInfo *PaymentCreationInfo) error { + + // Create the payment in the database. + err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error { + existingPayment, err := db.FetchPayment(ctx, paymentHash[:]) + switch { + // A payment with this hash already exists. We need to check its + // status to see if we can re-initialize. + case err == nil: + paymentStatus, err := computePaymentStatusFromDB( + ctx, s.cfg.QueryCfg, db, existingPayment, + ) + if err != nil { + return fmt.Errorf("failed to compute payment "+ + "status: %w", err) + } + + // Check if the payment is initializable otherwise + // we'll return early. + if err := paymentStatus.initializable(); err != nil { + return fmt.Errorf("payment is not "+ + "initializable: %w", err) + } + + // If the initializable check above passes, then the + // existing payment has failed. So we delete it and + // all of its previous artifacts. We rely on + // cascading deletes to clean up the rest. + err = db.DeletePayment(ctx, existingPayment.Payment.ID) + if err != nil { + return fmt.Errorf("failed to delete "+ + "payment: %w", err) + } + + // An unexpected error occurred while fetching the payment. + case !errors.Is(err, sql.ErrNoRows): + // Some other error occurred + return fmt.Errorf("failed to check existing "+ + "payment: %w", err) + + // The payment does not yet exist, so we can proceed. + default: + } + + // Insert the payment first to get its ID. + paymentID, err := db.InsertPayment( + ctx, sqlc.InsertPaymentParams{ + AmountMsat: int64( + paymentCreationInfo.Value, + ), + CreatedAt: paymentCreationInfo. + CreationTime.UTC(), + PaymentIdentifier: paymentHash[:], + }, + ) + if err != nil { + return fmt.Errorf("failed to insert payment: %w", err) + } + + // If there's a payment request, insert the payment intent. + if len(paymentCreationInfo.PaymentRequest) > 0 { + _, err = db.InsertPaymentIntent( + ctx, sqlc.InsertPaymentIntentParams{ + PaymentID: paymentID, + IntentType: int16( + PaymentIntentTypeBolt11, + ), + IntentPayload: paymentCreationInfo. + PaymentRequest, + }, + ) + if err != nil { + return fmt.Errorf("failed to insert "+ + "payment intent: %w", err) + } + } + + firstHopCustomRecords := paymentCreationInfo. + FirstHopCustomRecords + + for key, value := range firstHopCustomRecords { + err = db.InsertPaymentFirstHopCustomRecord( + ctx, + sqlc.InsertPaymentFirstHopCustomRecordParams{ + PaymentID: paymentID, + Key: int64(key), + Value: value, + }, + ) + if err != nil { + return fmt.Errorf("failed to insert "+ + "payment first hop custom "+ + "record: %w", err) + } + } + + return nil + }, sqldb.NoOpReset) + if err != nil { + return fmt.Errorf("failed to initialize payment: %w", err) + } + + return nil +} + +// insertRouteHops inserts all route hop data for a given set of hops. +func (s *SQLStore) insertRouteHops(ctx context.Context, db SQLQueries, + hops []*route.Hop, attemptID uint64) error { + + for i, hop := range hops { + // Insert the basic route hop data and get the generated ID. + hopID, err := db.InsertRouteHop(ctx, sqlc.InsertRouteHopParams{ + HtlcAttemptIndex: int64(attemptID), + HopIndex: int32(i), + PubKey: hop.PubKeyBytes[:], + Scid: strconv.FormatUint( + hop.ChannelID, 10, + ), + OutgoingTimeLock: int32(hop.OutgoingTimeLock), + AmtToForward: int64(hop.AmtToForward), + MetaData: hop.Metadata, + }) + if err != nil { + return fmt.Errorf("failed to insert route hop: %w", err) + } + + // Insert the per-hop custom records. + if len(hop.CustomRecords) > 0 { + for key, value := range hop.CustomRecords { + err = db.InsertPaymentHopCustomRecord( + ctx, + sqlc.InsertPaymentHopCustomRecordParams{ + HopID: hopID, + Key: int64(key), + Value: value, + }) + if err != nil { + return fmt.Errorf("failed to insert "+ + "payment hop custom record: %w", + err) + } + } + } + + // Insert MPP data if present. + if hop.MPP != nil { + paymentAddr := hop.MPP.PaymentAddr() + err = db.InsertRouteHopMpp( + ctx, sqlc.InsertRouteHopMppParams{ + HopID: hopID, + PaymentAddr: paymentAddr[:], + TotalMsat: int64(hop.MPP.TotalMsat()), + }) + if err != nil { + return fmt.Errorf("failed to insert "+ + "route hop MPP: %w", err) + } + } + + // Insert AMP data if present. + if hop.AMP != nil { + rootShare := hop.AMP.RootShare() + setID := hop.AMP.SetID() + err = db.InsertRouteHopAmp( + ctx, sqlc.InsertRouteHopAmpParams{ + HopID: hopID, + RootShare: rootShare[:], + SetID: setID[:], + ChildIndex: int32(hop.AMP.ChildIndex()), + }) + if err != nil { + return fmt.Errorf("failed to insert "+ + "route hop AMP: %w", err) + } + } + + // Insert blinded route data if present. Every hop in the + // blinded path must have an encrypted data record. If the + // encrypted data is not present, we skip the insertion. + if hop.EncryptedData == nil { + continue + } + + // The introduction point has a blinding point set. + var blindingPointBytes []byte + if hop.BlindingPoint != nil { + blindingPointBytes = hop.BlindingPoint. + SerializeCompressed() + } + + // The total amount is only set for the final hop in a + // blinded path. + totalAmtMsat := sql.NullInt64{} + if i == len(hops)-1 { + totalAmtMsat = sql.NullInt64{ + Int64: int64(hop.TotalAmtMsat), + Valid: true, + } + } + + err = db.InsertRouteHopBlinded(ctx, + sqlc.InsertRouteHopBlindedParams{ + HopID: hopID, + EncryptedData: hop.EncryptedData, + BlindingPoint: blindingPointBytes, + BlindedPathTotalAmt: totalAmtMsat, + }, + ) + if err != nil { + return fmt.Errorf("failed to insert "+ + "route hop blinded: %w", err) + } + } + + return nil +} + +// RegisterAttempt atomically records a new HTLC attempt for the specified +// payment. The attempt includes the attempt ID, session key, route information +// (hops, timelocks, amounts), and optional data such as MPP/AMP parameters, +// blinded route data, and custom records. +// +// Returns the updated MPPayment with the new attempt appended to the HTLCs +// slice, and the payment state recalculated. Returns an error if the payment +// doesn't exist or validation fails. +// +// This method is part of the PaymentControl interface, which is embedded in +// the PaymentWriter interface and ultimately the DB interface. It represents +// step 2 in the payment lifecycle control flow, called after InitPayment and +// potentially multiple times for multi-path payments. +func (s *SQLStore) RegisterAttempt(ctx context.Context, + paymentHash lntypes.Hash, attempt *HTLCAttemptInfo) (*MPPayment, + error) { + + var mpPayment *MPPayment + + err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error { + // Make sure the payment exists. + dbPayment, err := db.FetchPayment(ctx, paymentHash[:]) + if err != nil { + return err + } + + // We fetch the complete payment to determine if the payment is + // registrable. + // + // TODO(ziggie): We could improve the query here since only + // the last hop data is needed here not the complete payment + // data. + mpPayment, err = fetchPaymentWithCompleteData( + ctx, s.cfg.QueryCfg, db, dbPayment, + ) + if err != nil { + return fmt.Errorf("failed to fetch payment with "+ + "complete data: %w", err) + } + + if err := mpPayment.Registrable(); err != nil { + return fmt.Errorf("htlc attempt not registrable: %w", + err) + } + + // Verify the attempt is compatible with the existing payment. + if err := verifyAttempt(mpPayment, attempt); err != nil { + return fmt.Errorf("failed to verify attempt: %w", err) + } + + // Register the plain HTLC attempt next. + sessionKey := attempt.SessionKey() + sessionKeyBytes := sessionKey.Serialize() + + _, err = db.InsertHtlcAttempt(ctx, sqlc.InsertHtlcAttemptParams{ + PaymentID: dbPayment.Payment.ID, + AttemptIndex: int64(attempt.AttemptID), + SessionKey: sessionKeyBytes, + AttemptTime: attempt.AttemptTime, + PaymentHash: paymentHash[:], + FirstHopAmountMsat: int64( + attempt.Route.FirstHopAmount.Val.Int(), + ), + RouteTotalTimeLock: int32(attempt.Route.TotalTimeLock), + RouteTotalAmount: int64(attempt.Route.TotalAmount), + RouteSourceKey: attempt.Route.SourcePubKey[:], + }) + if err != nil { + return fmt.Errorf("failed to insert HTLC "+ + "attempt: %w", err) + } + + // Insert the route level first hop custom records. + attemptFirstHopCustomRecords := attempt.Route. + FirstHopWireCustomRecords + + for key, value := range attemptFirstHopCustomRecords { + //nolint:ll + err = db.InsertPaymentAttemptFirstHopCustomRecord( + ctx, + sqlc.InsertPaymentAttemptFirstHopCustomRecordParams{ + HtlcAttemptIndex: int64(attempt.AttemptID), + Key: int64(key), + Value: value, + }, + ) + if err != nil { + return fmt.Errorf("failed to insert "+ + "payment attempt first hop custom "+ + "record: %w", err) + } + } + + // Insert the route hops. + err = s.insertRouteHops( + ctx, db, attempt.Route.Hops, attempt.AttemptID, + ) + if err != nil { + return fmt.Errorf("failed to insert route hops: %w", + err) + } + + // We fetch the HTLC attempts again to recalculate the payment + // state after the attempt is registered. This also makes sure + // we have the right data in case multiple attempts are + // registered concurrently. + // + // NOTE: While the caller is responsible for serializing calls + // to RegisterAttempt per payment hash (see PaymentControl + // interface), we still refetch here to guarantee we return + // consistent, up-to-date data that reflects all changes made + // within this transaction. + mpPayment, err = fetchPaymentWithCompleteData( + ctx, s.cfg.QueryCfg, db, dbPayment, + ) + if err != nil { + return fmt.Errorf("failed to fetch payment with "+ + "complete data: %w", err) + } + + return nil + }, func() { + mpPayment = nil + }) + if err != nil { + return nil, fmt.Errorf("failed to register attempt: %w", err) + } + + return mpPayment, nil +} + +// SettleAttempt marks the specified HTLC attempt as successfully settled, +// recording the payment preimage and settlement time. The preimage serves as +// cryptographic proof of payment and is atomically saved to the database. +// +// This method is part of the PaymentControl interface, which is embedded in +// the PaymentWriter interface and ultimately the DB interface. It represents +// step 3a in the payment lifecycle control flow (step 3b is FailAttempt), +// called after RegisterAttempt when an HTLC successfully completes. +func (s *SQLStore) SettleAttempt(ctx context.Context, paymentHash lntypes.Hash, + attemptID uint64, settleInfo *HTLCSettleInfo) (*MPPayment, error) { + + var mpPayment *MPPayment + + err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error { + dbPayment, err := fetchPaymentByHash(ctx, db, paymentHash) + if err != nil { + return err + } + + paymentStatus, err := computePaymentStatusFromDB( + ctx, s.cfg.QueryCfg, db, dbPayment, + ) + if err != nil { + return fmt.Errorf("failed to compute payment "+ + "status: %w", err) + } + + if err := paymentStatus.updatable(); err != nil { + return fmt.Errorf("payment is not updatable: %w", err) + } + + err = db.SettleAttempt(ctx, sqlc.SettleAttemptParams{ + AttemptIndex: int64(attemptID), + ResolutionTime: time.Now(), + ResolutionType: int32(HTLCAttemptResolutionSettled), + SettlePreimage: settleInfo.Preimage[:], + }) + if err != nil { + return fmt.Errorf("failed to settle attempt: %w", err) + } + + // Fetch the complete payment after we settled the attempt. + mpPayment, err = fetchPaymentWithCompleteData( + ctx, s.cfg.QueryCfg, db, dbPayment, + ) + if err != nil { + return fmt.Errorf("failed to fetch payment with "+ + "complete data: %w", err) + } + + return nil + }, func() { + mpPayment = nil + }) + if err != nil { + return nil, fmt.Errorf("failed to settle attempt: %w", err) + } + + return mpPayment, nil +} + +// FailAttempt marks the specified HTLC attempt as failed, recording the +// failure reason, failure time, optional failure message, and the index of the +// node in the route that generated the failure. This information is atomically +// saved to the database for debugging and route optimization purposes. +// +// For single-path payments, failing the only attempt may lead to the payment +// being retried or ultimately failed via the Fail method. For multi-shard +// (MPP/AMP) payments, individual shard failures don't necessarily fail the +// entire payment; additional attempts can be registered until sufficient shards +// succeed or the payment is permanently failed. +// +// Returns the updated MPPayment with the attempt marked as failed and the +// payment state recalculated. The payment status remains StatusInFlight if +// other attempts are still in flight, or may transition based on the overall +// payment state. +// +// This method is part of the PaymentControl interface, which is embedded in +// the PaymentWriter interface and ultimately the DB interface. It represents +// step 3b in the payment lifecycle control flow (step 3a is SettleAttempt), +// called after RegisterAttempt when an HTLC fails. +func (s *SQLStore) FailAttempt(ctx context.Context, paymentHash lntypes.Hash, + attemptID uint64, failInfo *HTLCFailInfo) (*MPPayment, error) { + + var mpPayment *MPPayment + + err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error { + // Make sure the payment exists. + dbPayment, err := fetchPaymentByHash(ctx, db, paymentHash) + if err != nil { + return err + } + + paymentStatus, err := computePaymentStatusFromDB( + ctx, s.cfg.QueryCfg, db, dbPayment, + ) + if err != nil { + return fmt.Errorf("failed to compute payment "+ + "status: %w", err) + } + + // We check if the payment is updatable before failing the + // attempt. + if err := paymentStatus.updatable(); err != nil { + return fmt.Errorf("payment is not updatable: %w", err) + } + + var failureMsg bytes.Buffer + if failInfo.Message != nil { + err := lnwire.EncodeFailureMessage( + &failureMsg, failInfo.Message, 0, + ) + if err != nil { + return fmt.Errorf("failed to encode "+ + "failure message: %w", err) + } + } + + err = db.FailAttempt(ctx, sqlc.FailAttemptParams{ + AttemptIndex: int64(attemptID), + ResolutionTime: time.Now(), + ResolutionType: int32(HTLCAttemptResolutionFailed), + FailureSourceIndex: sqldb.SQLInt32( + failInfo.FailureSourceIndex, + ), + HtlcFailReason: sqldb.SQLInt32(failInfo.Reason), + FailureMsg: failureMsg.Bytes(), + }) + if err != nil { + return fmt.Errorf("failed to fail attempt: %w", err) + } + + mpPayment, err = fetchPaymentWithCompleteData( + ctx, s.cfg.QueryCfg, db, dbPayment, + ) + if err != nil { + return fmt.Errorf("failed to fetch payment with "+ + "complete data: %w", err) + } + + return nil + }, func() { + mpPayment = nil + }) + if err != nil { + return nil, fmt.Errorf("failed to fail attempt: %w", err) + } + + return mpPayment, nil +} + +// Fail records the ultimate reason why a payment failed. This method stores +// the failure reason for record keeping but does not enforce that all HTLC +// attempts are resolved - HTLCs may still be in flight when this is called. +// +// The payment's actual status transition to StatusFailed is determined by the +// payment state calculation, which considers both the recorded failure reason +// and the current state of all HTLC attempts. The status will transition to +// StatusFailed once all HTLCs are resolved and/or a failure reason is recorded. +// +// NOTE: According to the interface contract, this should only be called when +// all active attempts are already failed. However, the implementation allows +// concurrent calls and does not validate this precondition, enabling the last +// failing attempt to record the failure reason without synchronization. +// +// This method is part of the PaymentControl interface, which is embedded in +// the PaymentWriter interface and ultimately the DB interface. It represents +// step 4 in the payment lifecycle control flow. +func (s *SQLStore) Fail(ctx context.Context, paymentHash lntypes.Hash, + reason FailureReason) (*MPPayment, error) { + + var mpPayment *MPPayment + + err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error { + result, err := db.FailPayment(ctx, sqlc.FailPaymentParams{ + PaymentIdentifier: paymentHash[:], + FailReason: sqldb.SQLInt32(reason), + }) + if err != nil { + return err + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return err + } + if rowsAffected == 0 { + return ErrPaymentNotInitiated + } + + payment, err := db.FetchPayment(ctx, paymentHash[:]) + if err != nil { + return fmt.Errorf("failed to fetch payment: %w", err) + } + mpPayment, err = fetchPaymentWithCompleteData( + ctx, s.cfg.QueryCfg, db, payment, + ) + if err != nil { + return fmt.Errorf("failed to fetch payment with "+ + "complete data: %w", err) + } + + return nil + }, func() { + mpPayment = nil + }) + if err != nil { + return nil, fmt.Errorf("failed to fail payment: %w", err) + } + + return mpPayment, nil +} + +// DeletePayments performs a batch deletion of payments or their failed HTLC +// attempts from the database based on the specified flags. This is a bulk +// operation that iterates through all payments and selectively deletes based +// on the criteria. +// The behavior is controlled by two flags: +// +// If failedAttemptsOnly is true, only failed HTLC attempts are deleted while +// preserving the payment records and any successful or in-flight attempts. +// The return value is always 0 when deleting attempts only. +// +// If failedAttemptsOnly is false, entire payment records are deleted including +// all associated data (HTLCs, metadata, intents). The return value is the +// number of payments deleted. +// +// The failedOnly flag further filters which payments are processed: +// - failedOnly=true, failedAttemptsOnly=true: Delete failed attempts for +// StatusFailed payments only +// - failedOnly=false, failedAttemptsOnly=true: Delete failed attempts for +// all removable payments +// - failedOnly=true, failedAttemptsOnly=false: Delete entire payment records +// for StatusFailed payments only +// - failedOnly=false, failedAttemptsOnly=false: Delete all removable payment +// records (StatusInitiated, StatusSucceeded, StatusFailed) +// +// Safety checks applied to all operations: +// - Payments with StatusInFlight are always skipped (cannot be safely deleted +// while HTLCs are on the network) +// - The payment status must pass the removable() check +// +// Returns the number of complete payments deleted (0 if only deleting failed +// attempts). This is useful for cleanup operations, administrative maintenance, +// or freeing up database storage. +// +// This method is part of the PaymentWriter interface, which is embedded in +// the DB interface. +// +// TODO(ziggie): batch and use iterator instead, moreover we dont need to fetch +// the complete payment data for each payment, we can just fetch the payment ID +// and the resolution types to decide if the payment is removable. +func (s *SQLStore) DeletePayments(ctx context.Context, failedOnly, + failedHtlcsOnly bool) (int, error) { + + var numPayments int + + extractCursor := func(row sqlc.FilterPaymentsRow) int64 { + return row.Payment.ID + } + + err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error { + // collectFunc extracts the payment ID from each payment row. + collectFunc := func(row sqlc.FilterPaymentsRow) (int64, error) { + return row.Payment.ID, nil + } + + // batchDataFunc loads only HTLC resolution types for a batch + // of payments, which is sufficient to determine payment status. + batchDataFunc := func(ctx context.Context, paymentIDs []int64) ( + *paymentStatusData, error) { + + return batchLoadPaymentResolutions( + ctx, s.cfg.QueryCfg, db, paymentIDs, + ) + } + + // processPayment processes each payment with the lightweight + // batch-loaded resolution data. + processPayment := func(ctx context.Context, + dbPayment sqlc.FilterPaymentsRow, + batchData *paymentStatusData) error { + + payment := dbPayment.Payment + + // Compute the payment status from resolution types and + // failure reason without building the complete payment. + resolutionTypes := batchData.resolutionTypes[payment.ID] + status, err := computePaymentStatusFromResolutions( + resolutionTypes, payment.FailReason, + ) + if err != nil { + return fmt.Errorf("failed to compute payment "+ + "status: %w", err) + } + + // Payments which are not final yet cannot be deleted. + // we skip them. + if err := status.removable(); err != nil { + return nil + } + + // If we are only deleting failed payments, we skip + // if the payment is not failed. + if failedOnly && status != StatusFailed { + return nil + } + + // If we are only deleting failed HTLCs, we delete them + // and return early. + if failedHtlcsOnly { + return db.DeleteFailedAttempts( + ctx, payment.ID, + ) + } + + // Otherwise we delete the payment. + err = db.DeletePayment(ctx, payment.ID) + if err != nil { + return fmt.Errorf("failed to delete "+ + "payment: %w", err) + } + + numPayments++ + + return nil + } + + queryFunc := func(ctx context.Context, lastID int64, + limit int32) ([]sqlc.FilterPaymentsRow, error) { + + filterParams := sqlc.FilterPaymentsParams{ + NumLimit: limit, + IndexOffsetGet: sqldb.SQLInt64( + lastID, + ), + } + + return db.FilterPayments(ctx, filterParams) + } + + return sqldb.ExecuteCollectAndBatchWithSharedDataQuery( + ctx, s.cfg.QueryCfg, int64(-1), queryFunc, + extractCursor, collectFunc, batchDataFunc, + processPayment, + ) + }, func() { + numPayments = 0 + }) + if err != nil { + return 0, fmt.Errorf("failed to delete payments "+ + "(failedOnly: %v, failedHtlcsOnly: %v): %w", + failedOnly, failedHtlcsOnly, err) + } + + return numPayments, nil +} diff --git a/payments/db/migration1/sqlc/db.go b/payments/db/migration1/sqlc/db.go new file mode 100644 index 00000000000..e4d78283b21 --- /dev/null +++ b/payments/db/migration1/sqlc/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 + +package sqlc + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/payments/db/migration1/sqlc/db_custom.go b/payments/db/migration1/sqlc/db_custom.go new file mode 100644 index 00000000000..625e65adcca --- /dev/null +++ b/payments/db/migration1/sqlc/db_custom.go @@ -0,0 +1,123 @@ +package sqlc + +import ( + "fmt" + "strings" +) + +// GetTx returns the underlying DBTX (either *sql.DB or *sql.Tx) used by the +// Queries struct. +func (q *Queries) GetTx() DBTX { + return q.db +} + +// makeQueryParams generates a string of query parameters for a SQL query. It is +// meant to replace the `?` placeholders in a SQL query with numbered parameters +// like `$1`, `$2`, etc. This is required for the sqlc /*SLICE:*/ +// workaround. See scripts/gen_sqlc_docker.sh for more details. +func makeQueryParams(numTotalArgs, numListArgs int) string { + if numListArgs == 0 { + return "" + } + + var b strings.Builder + + // Pre-allocate a rough estimation of the buffer size to avoid + // re-allocations. A parameter like $1000, takes 6 bytes. + b.Grow(numListArgs * 6) + + diff := numTotalArgs - numListArgs + for i := 0; i < numListArgs; i++ { + if i > 0 { + // We don't need to check the error here because the + // WriteString method of strings.Builder always returns + // nil. + _, _ = b.WriteString(",") + } + + // We don't need to check the error here because the + // Write method (called by fmt.Fprintf) of strings.Builder + // always returns nil. + _, _ = fmt.Fprintf(&b, "$%d", i+diff+1) + } + + return b.String() +} + +// PaymentAndIntent is an interface that provides access to a payment and its +// associated payment intent. +type PaymentAndIntent interface { + // GetPayment returns the Payment associated with this interface. + GetPayment() Payment + + // GetPaymentIntent returns the PaymentIntent associated with this + // payment. + GetPaymentIntent() PaymentIntent +} + +// GetPayment returns the Payment associated with this interface. +// +// NOTE: This method is part of the PaymentAndIntent interface. +func (r FilterPaymentsRow) GetPayment() Payment { + return r.Payment +} + +// GetPaymentIntent returns the PaymentIntent associated with this payment. +// If the payment has no intent (IntentType is NULL), this returns a zero-value +// PaymentIntent. +// +// NOTE: This method is part of the PaymentAndIntent interface. +func (r FilterPaymentsRow) GetPaymentIntent() PaymentIntent { + if !r.IntentType.Valid { + return PaymentIntent{} + } + + return PaymentIntent{ + IntentType: r.IntentType.Int16, + IntentPayload: r.IntentPayload, + } +} + +// GetPayment returns the Payment associated with this interface. +// +// NOTE: This method is part of the PaymentAndIntent interface. +func (r FetchPaymentRow) GetPayment() Payment { + return r.Payment +} + +// GetPaymentIntent returns the PaymentIntent associated with this payment. +// If the payment has no intent (IntentType is NULL), this returns a zero-value +// PaymentIntent. +// +// NOTE: This method is part of the PaymentAndIntent interface. +func (r FetchPaymentRow) GetPaymentIntent() PaymentIntent { + if !r.IntentType.Valid { + return PaymentIntent{} + } + + return PaymentIntent{ + IntentType: r.IntentType.Int16, + IntentPayload: r.IntentPayload, + } +} + +func (r FetchPaymentsByIDsRow) GetPayment() Payment { + return Payment{ + ID: r.ID, + AmountMsat: r.AmountMsat, + CreatedAt: r.CreatedAt, + PaymentIdentifier: r.PaymentIdentifier, + FailReason: r.FailReason, + } +} + +func (r FetchPaymentsByIDsRow) GetPaymentIntent() PaymentIntent { + if !r.IntentType.Valid { + return PaymentIntent{} + } + + return PaymentIntent{ + IntentType: r.IntentType.Int16, + IntentPayload: r.IntentPayload, + } +} diff --git a/payments/db/migration1/sqlc/models.go b/payments/db/migration1/sqlc/models.go new file mode 100644 index 00000000000..6faa701d99a --- /dev/null +++ b/payments/db/migration1/sqlc/models.go @@ -0,0 +1,111 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 + +package sqlc + +import ( + "database/sql" + "time" +) + +type Payment struct { + ID int64 + AmountMsat int64 + CreatedAt time.Time + PaymentIdentifier []byte + FailReason sql.NullInt32 +} + +type PaymentAttemptFirstHopCustomRecord struct { + ID int64 + HtlcAttemptIndex int64 + Key int64 + Value []byte +} + +type PaymentDuplicate struct { + ID int64 + PaymentID int64 + PaymentIdentifier []byte + AmountMsat int64 + CreatedAt time.Time + FailReason sql.NullInt32 + SettlePreimage []byte + SettleTime sql.NullTime +} + +type PaymentFirstHopCustomRecord struct { + ID int64 + PaymentID int64 + Key int64 + Value []byte +} + +type PaymentHopCustomRecord struct { + ID int64 + HopID int64 + Key int64 + Value []byte +} + +type PaymentHtlcAttempt struct { + ID int64 + AttemptIndex int64 + PaymentID int64 + SessionKey []byte + AttemptTime time.Time + PaymentHash []byte + FirstHopAmountMsat int64 + RouteTotalTimeLock int32 + RouteTotalAmount int64 + RouteSourceKey []byte +} + +type PaymentHtlcAttemptResolution struct { + AttemptIndex int64 + ResolutionTime time.Time + ResolutionType int32 + SettlePreimage []byte + FailureSourceIndex sql.NullInt32 + HtlcFailReason sql.NullInt32 + FailureMsg []byte +} + +type PaymentIntent struct { + ID int64 + PaymentID int64 + IntentType int16 + IntentPayload []byte +} + +type PaymentRouteHop struct { + ID int64 + HtlcAttemptIndex int64 + HopIndex int32 + PubKey []byte + Scid string + OutgoingTimeLock int32 + AmtToForward int64 + MetaData []byte +} + +type PaymentRouteHopAmp struct { + HopID int64 + RootShare []byte + SetID []byte + ChildIndex int32 +} + +type PaymentRouteHopBlinded struct { + HopID int64 + EncryptedData []byte + BlindingPoint []byte + BlindedPathTotalAmt sql.NullInt64 +} + +type PaymentRouteHopMpp struct { + HopID int64 + PaymentAddr []byte + TotalMsat int64 +} diff --git a/payments/db/migration1/sqlc/payments.sql.go b/payments/db/migration1/sqlc/payments.sql.go new file mode 100644 index 00000000000..e2ad4bcb6f6 --- /dev/null +++ b/payments/db/migration1/sqlc/payments.sql.go @@ -0,0 +1,1227 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: payments.sql + +package sqlc + +import ( + "context" + "database/sql" + "strings" + "time" +) + +const countPayments = `-- name: CountPayments :one +SELECT COUNT(*) FROM payments +` + +func (q *Queries) CountPayments(ctx context.Context) (int64, error) { + row := q.db.QueryRowContext(ctx, countPayments) + var count int64 + err := row.Scan(&count) + return count, err +} + +const deleteFailedAttempts = `-- name: DeleteFailedAttempts :exec +DELETE FROM payment_htlc_attempts WHERE payment_id = $1 AND attempt_index IN ( + SELECT attempt_index FROM payment_htlc_attempt_resolutions WHERE resolution_type = 2 +) +` + +// Delete all failed HTLC attempts for the given payment. Resolution type 2 +// indicates a failed attempt. +func (q *Queries) DeleteFailedAttempts(ctx context.Context, paymentID int64) error { + _, err := q.db.ExecContext(ctx, deleteFailedAttempts, paymentID) + return err +} + +const deletePayment = `-- name: DeletePayment :exec +DELETE FROM payments WHERE id = $1 +` + +func (q *Queries) DeletePayment(ctx context.Context, id int64) error { + _, err := q.db.ExecContext(ctx, deletePayment, id) + return err +} + +const failAttempt = `-- name: FailAttempt :exec +INSERT INTO payment_htlc_attempt_resolutions ( + attempt_index, + resolution_time, + resolution_type, + failure_source_index, + htlc_fail_reason, + failure_msg +) +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6 +) +` + +type FailAttemptParams struct { + AttemptIndex int64 + ResolutionTime time.Time + ResolutionType int32 + FailureSourceIndex sql.NullInt32 + HtlcFailReason sql.NullInt32 + FailureMsg []byte +} + +func (q *Queries) FailAttempt(ctx context.Context, arg FailAttemptParams) error { + _, err := q.db.ExecContext(ctx, failAttempt, + arg.AttemptIndex, + arg.ResolutionTime, + arg.ResolutionType, + arg.FailureSourceIndex, + arg.HtlcFailReason, + arg.FailureMsg, + ) + return err +} + +const failPayment = `-- name: FailPayment :execresult +UPDATE payments SET fail_reason = $1 WHERE payment_identifier = $2 +` + +type FailPaymentParams struct { + FailReason sql.NullInt32 + PaymentIdentifier []byte +} + +func (q *Queries) FailPayment(ctx context.Context, arg FailPaymentParams) (sql.Result, error) { + return q.db.ExecContext(ctx, failPayment, arg.FailReason, arg.PaymentIdentifier) +} + +const fetchAllInflightAttempts = `-- name: FetchAllInflightAttempts :many +SELECT + ha.id, + ha.attempt_index, + ha.payment_id, + ha.session_key, + ha.attempt_time, + ha.payment_hash, + ha.first_hop_amount_msat, + ha.route_total_time_lock, + ha.route_total_amount, + ha.route_source_key +FROM payment_htlc_attempts ha +WHERE NOT EXISTS ( + SELECT 1 FROM payment_htlc_attempt_resolutions hr + WHERE hr.attempt_index = ha.attempt_index +) +AND ha.attempt_index > $1 +ORDER BY ha.attempt_index ASC +LIMIT $2 +` + +type FetchAllInflightAttemptsParams struct { + AttemptIndex int64 + Limit int32 +} + +// Fetch all inflight attempts with their payment data using pagination. +// Returns attempt data joined with payment and intent data to avoid separate queries. +func (q *Queries) FetchAllInflightAttempts(ctx context.Context, arg FetchAllInflightAttemptsParams) ([]PaymentHtlcAttempt, error) { + rows, err := q.db.QueryContext(ctx, fetchAllInflightAttempts, arg.AttemptIndex, arg.Limit) + if err != nil { + return nil, err + } + defer rows.Close() + var items []PaymentHtlcAttempt + for rows.Next() { + var i PaymentHtlcAttempt + if err := rows.Scan( + &i.ID, + &i.AttemptIndex, + &i.PaymentID, + &i.SessionKey, + &i.AttemptTime, + &i.PaymentHash, + &i.FirstHopAmountMsat, + &i.RouteTotalTimeLock, + &i.RouteTotalAmount, + &i.RouteSourceKey, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const fetchHopLevelCustomRecords = `-- name: FetchHopLevelCustomRecords :many +SELECT + l.id, + l.hop_id, + l.key, + l.value +FROM payment_hop_custom_records l +WHERE l.hop_id IN (/*SLICE:hop_ids*/?) +ORDER BY l.hop_id ASC, l.key ASC +` + +func (q *Queries) FetchHopLevelCustomRecords(ctx context.Context, hopIds []int64) ([]PaymentHopCustomRecord, error) { + query := fetchHopLevelCustomRecords + var queryParams []interface{} + if len(hopIds) > 0 { + for _, v := range hopIds { + queryParams = append(queryParams, v) + } + query = strings.Replace(query, "/*SLICE:hop_ids*/?", makeQueryParams(len(queryParams), len(hopIds)), 1) + } else { + query = strings.Replace(query, "/*SLICE:hop_ids*/?", "NULL", 1) + } + rows, err := q.db.QueryContext(ctx, query, queryParams...) + if err != nil { + return nil, err + } + defer rows.Close() + var items []PaymentHopCustomRecord + for rows.Next() { + var i PaymentHopCustomRecord + if err := rows.Scan( + &i.ID, + &i.HopID, + &i.Key, + &i.Value, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const fetchHopsForAttempts = `-- name: FetchHopsForAttempts :many +SELECT + h.id, + h.htlc_attempt_index, + h.hop_index, + h.pub_key, + h.scid, + h.outgoing_time_lock, + h.amt_to_forward, + h.meta_data, + m.payment_addr AS mpp_payment_addr, + m.total_msat AS mpp_total_msat, + a.root_share AS amp_root_share, + a.set_id AS amp_set_id, + a.child_index AS amp_child_index, + b.encrypted_data, + b.blinding_point, + b.blinded_path_total_amt +FROM payment_route_hops h +LEFT JOIN payment_route_hop_mpp m ON m.hop_id = h.id +LEFT JOIN payment_route_hop_amp a ON a.hop_id = h.id +LEFT JOIN payment_route_hop_blinded b ON b.hop_id = h.id +WHERE h.htlc_attempt_index IN (/*SLICE:htlc_attempt_indices*/?) +ORDER BY h.htlc_attempt_index ASC, h.hop_index ASC +` + +type FetchHopsForAttemptsRow struct { + ID int64 + HtlcAttemptIndex int64 + HopIndex int32 + PubKey []byte + Scid string + OutgoingTimeLock int32 + AmtToForward int64 + MetaData []byte + MppPaymentAddr []byte + MppTotalMsat sql.NullInt64 + AmpRootShare []byte + AmpSetID []byte + AmpChildIndex sql.NullInt32 + EncryptedData []byte + BlindingPoint []byte + BlindedPathTotalAmt sql.NullInt64 +} + +func (q *Queries) FetchHopsForAttempts(ctx context.Context, htlcAttemptIndices []int64) ([]FetchHopsForAttemptsRow, error) { + query := fetchHopsForAttempts + var queryParams []interface{} + if len(htlcAttemptIndices) > 0 { + for _, v := range htlcAttemptIndices { + queryParams = append(queryParams, v) + } + query = strings.Replace(query, "/*SLICE:htlc_attempt_indices*/?", makeQueryParams(len(queryParams), len(htlcAttemptIndices)), 1) + } else { + query = strings.Replace(query, "/*SLICE:htlc_attempt_indices*/?", "NULL", 1) + } + rows, err := q.db.QueryContext(ctx, query, queryParams...) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FetchHopsForAttemptsRow + for rows.Next() { + var i FetchHopsForAttemptsRow + if err := rows.Scan( + &i.ID, + &i.HtlcAttemptIndex, + &i.HopIndex, + &i.PubKey, + &i.Scid, + &i.OutgoingTimeLock, + &i.AmtToForward, + &i.MetaData, + &i.MppPaymentAddr, + &i.MppTotalMsat, + &i.AmpRootShare, + &i.AmpSetID, + &i.AmpChildIndex, + &i.EncryptedData, + &i.BlindingPoint, + &i.BlindedPathTotalAmt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const fetchHtlcAttemptResolutionsForPayments = `-- name: FetchHtlcAttemptResolutionsForPayments :many +SELECT + ha.payment_id, + hr.resolution_type +FROM payment_htlc_attempts ha +LEFT JOIN payment_htlc_attempt_resolutions hr ON hr.attempt_index = ha.attempt_index +WHERE ha.payment_id IN (/*SLICE:payment_ids*/?) +` + +type FetchHtlcAttemptResolutionsForPaymentsRow struct { + PaymentID int64 + ResolutionType sql.NullInt32 +} + +// Batch query to fetch only HTLC resolution status for multiple payments. +// We don't need to order by payment_id and attempt_time because we will +// group the resolutions by payment_id in the background. +func (q *Queries) FetchHtlcAttemptResolutionsForPayments(ctx context.Context, paymentIds []int64) ([]FetchHtlcAttemptResolutionsForPaymentsRow, error) { + query := fetchHtlcAttemptResolutionsForPayments + var queryParams []interface{} + if len(paymentIds) > 0 { + for _, v := range paymentIds { + queryParams = append(queryParams, v) + } + query = strings.Replace(query, "/*SLICE:payment_ids*/?", makeQueryParams(len(queryParams), len(paymentIds)), 1) + } else { + query = strings.Replace(query, "/*SLICE:payment_ids*/?", "NULL", 1) + } + rows, err := q.db.QueryContext(ctx, query, queryParams...) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FetchHtlcAttemptResolutionsForPaymentsRow + for rows.Next() { + var i FetchHtlcAttemptResolutionsForPaymentsRow + if err := rows.Scan(&i.PaymentID, &i.ResolutionType); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const fetchHtlcAttemptsForPayments = `-- name: FetchHtlcAttemptsForPayments :many +SELECT + ha.id, + ha.attempt_index, + ha.payment_id, + ha.session_key, + ha.attempt_time, + ha.payment_hash, + ha.first_hop_amount_msat, + ha.route_total_time_lock, + ha.route_total_amount, + ha.route_source_key, + hr.resolution_type, + hr.resolution_time, + hr.failure_source_index, + hr.htlc_fail_reason, + hr.failure_msg, + hr.settle_preimage +FROM payment_htlc_attempts ha +LEFT JOIN payment_htlc_attempt_resolutions hr ON hr.attempt_index = ha.attempt_index +WHERE ha.payment_id IN (/*SLICE:payment_ids*/?) +ORDER BY ha.payment_id ASC, ha.attempt_time ASC +` + +type FetchHtlcAttemptsForPaymentsRow struct { + ID int64 + AttemptIndex int64 + PaymentID int64 + SessionKey []byte + AttemptTime time.Time + PaymentHash []byte + FirstHopAmountMsat int64 + RouteTotalTimeLock int32 + RouteTotalAmount int64 + RouteSourceKey []byte + ResolutionType sql.NullInt32 + ResolutionTime sql.NullTime + FailureSourceIndex sql.NullInt32 + HtlcFailReason sql.NullInt32 + FailureMsg []byte + SettlePreimage []byte +} + +func (q *Queries) FetchHtlcAttemptsForPayments(ctx context.Context, paymentIds []int64) ([]FetchHtlcAttemptsForPaymentsRow, error) { + query := fetchHtlcAttemptsForPayments + var queryParams []interface{} + if len(paymentIds) > 0 { + for _, v := range paymentIds { + queryParams = append(queryParams, v) + } + query = strings.Replace(query, "/*SLICE:payment_ids*/?", makeQueryParams(len(queryParams), len(paymentIds)), 1) + } else { + query = strings.Replace(query, "/*SLICE:payment_ids*/?", "NULL", 1) + } + rows, err := q.db.QueryContext(ctx, query, queryParams...) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FetchHtlcAttemptsForPaymentsRow + for rows.Next() { + var i FetchHtlcAttemptsForPaymentsRow + if err := rows.Scan( + &i.ID, + &i.AttemptIndex, + &i.PaymentID, + &i.SessionKey, + &i.AttemptTime, + &i.PaymentHash, + &i.FirstHopAmountMsat, + &i.RouteTotalTimeLock, + &i.RouteTotalAmount, + &i.RouteSourceKey, + &i.ResolutionType, + &i.ResolutionTime, + &i.FailureSourceIndex, + &i.HtlcFailReason, + &i.FailureMsg, + &i.SettlePreimage, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const fetchPayment = `-- name: FetchPayment :one +SELECT + p.id, p.amount_msat, p.created_at, p.payment_identifier, p.fail_reason, + i.intent_type AS "intent_type", + i.intent_payload AS "intent_payload" +FROM payments p +LEFT JOIN payment_intents i ON i.payment_id = p.id +WHERE p.payment_identifier = $1 +` + +type FetchPaymentRow struct { + Payment Payment + IntentType sql.NullInt16 + IntentPayload []byte +} + +func (q *Queries) FetchPayment(ctx context.Context, paymentIdentifier []byte) (FetchPaymentRow, error) { + row := q.db.QueryRowContext(ctx, fetchPayment, paymentIdentifier) + var i FetchPaymentRow + err := row.Scan( + &i.Payment.ID, + &i.Payment.AmountMsat, + &i.Payment.CreatedAt, + &i.Payment.PaymentIdentifier, + &i.Payment.FailReason, + &i.IntentType, + &i.IntentPayload, + ) + return i, err +} + +const fetchPaymentDuplicates = `-- name: FetchPaymentDuplicates :many +SELECT + id, + payment_id, + payment_identifier, + amount_msat, + created_at, + fail_reason, + settle_preimage, + settle_time +FROM payment_duplicates +WHERE payment_id = $1 +ORDER BY id ASC +` + +func (q *Queries) FetchPaymentDuplicates(ctx context.Context, paymentID int64) ([]PaymentDuplicate, error) { + rows, err := q.db.QueryContext(ctx, fetchPaymentDuplicates, paymentID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []PaymentDuplicate + for rows.Next() { + var i PaymentDuplicate + if err := rows.Scan( + &i.ID, + &i.PaymentID, + &i.PaymentIdentifier, + &i.AmountMsat, + &i.CreatedAt, + &i.FailReason, + &i.SettlePreimage, + &i.SettleTime, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const fetchPaymentLevelFirstHopCustomRecords = `-- name: FetchPaymentLevelFirstHopCustomRecords :many +SELECT + l.id, + l.payment_id, + l.key, + l.value +FROM payment_first_hop_custom_records l +WHERE l.payment_id IN (/*SLICE:payment_ids*/?) +ORDER BY l.payment_id ASC, l.key ASC +` + +func (q *Queries) FetchPaymentLevelFirstHopCustomRecords(ctx context.Context, paymentIds []int64) ([]PaymentFirstHopCustomRecord, error) { + query := fetchPaymentLevelFirstHopCustomRecords + var queryParams []interface{} + if len(paymentIds) > 0 { + for _, v := range paymentIds { + queryParams = append(queryParams, v) + } + query = strings.Replace(query, "/*SLICE:payment_ids*/?", makeQueryParams(len(queryParams), len(paymentIds)), 1) + } else { + query = strings.Replace(query, "/*SLICE:payment_ids*/?", "NULL", 1) + } + rows, err := q.db.QueryContext(ctx, query, queryParams...) + if err != nil { + return nil, err + } + defer rows.Close() + var items []PaymentFirstHopCustomRecord + for rows.Next() { + var i PaymentFirstHopCustomRecord + if err := rows.Scan( + &i.ID, + &i.PaymentID, + &i.Key, + &i.Value, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const fetchPaymentsByIDs = `-- name: FetchPaymentsByIDs :many +SELECT + p.id, + p.amount_msat, + p.created_at, + p.payment_identifier, + p.fail_reason, + pi.intent_type, + pi.intent_payload +FROM payments p +LEFT JOIN payment_intents pi ON pi.payment_id = p.id +WHERE p.id IN (/*SLICE:payment_ids*/?) +ORDER BY p.id ASC +` + +type FetchPaymentsByIDsRow struct { + ID int64 + AmountMsat int64 + CreatedAt time.Time + PaymentIdentifier []byte + FailReason sql.NullInt32 + IntentType sql.NullInt16 + IntentPayload []byte +} + +// Batch fetch payment and intent data for a set of payment IDs. +// Used to avoid fetching redundant payment data when processing multiple +// attempts for the same payment. +func (q *Queries) FetchPaymentsByIDs(ctx context.Context, paymentIds []int64) ([]FetchPaymentsByIDsRow, error) { + query := fetchPaymentsByIDs + var queryParams []interface{} + if len(paymentIds) > 0 { + for _, v := range paymentIds { + queryParams = append(queryParams, v) + } + query = strings.Replace(query, "/*SLICE:payment_ids*/?", makeQueryParams(len(queryParams), len(paymentIds)), 1) + } else { + query = strings.Replace(query, "/*SLICE:payment_ids*/?", "NULL", 1) + } + rows, err := q.db.QueryContext(ctx, query, queryParams...) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FetchPaymentsByIDsRow + for rows.Next() { + var i FetchPaymentsByIDsRow + if err := rows.Scan( + &i.ID, + &i.AmountMsat, + &i.CreatedAt, + &i.PaymentIdentifier, + &i.FailReason, + &i.IntentType, + &i.IntentPayload, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const fetchRouteLevelFirstHopCustomRecords = `-- name: FetchRouteLevelFirstHopCustomRecords :many +SELECT + l.id, + l.htlc_attempt_index, + l.key, + l.value +FROM payment_attempt_first_hop_custom_records l +WHERE l.htlc_attempt_index IN (/*SLICE:htlc_attempt_indices*/?) +ORDER BY l.htlc_attempt_index ASC, l.key ASC +` + +func (q *Queries) FetchRouteLevelFirstHopCustomRecords(ctx context.Context, htlcAttemptIndices []int64) ([]PaymentAttemptFirstHopCustomRecord, error) { + query := fetchRouteLevelFirstHopCustomRecords + var queryParams []interface{} + if len(htlcAttemptIndices) > 0 { + for _, v := range htlcAttemptIndices { + queryParams = append(queryParams, v) + } + query = strings.Replace(query, "/*SLICE:htlc_attempt_indices*/?", makeQueryParams(len(queryParams), len(htlcAttemptIndices)), 1) + } else { + query = strings.Replace(query, "/*SLICE:htlc_attempt_indices*/?", "NULL", 1) + } + rows, err := q.db.QueryContext(ctx, query, queryParams...) + if err != nil { + return nil, err + } + defer rows.Close() + var items []PaymentAttemptFirstHopCustomRecord + for rows.Next() { + var i PaymentAttemptFirstHopCustomRecord + if err := rows.Scan( + &i.ID, + &i.HtlcAttemptIndex, + &i.Key, + &i.Value, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const filterPayments = `-- name: FilterPayments :many +/* ───────────────────────────────────────────── + fetch queries + ───────────────────────────────────────────── +*/ + +SELECT + p.id, p.amount_msat, p.created_at, p.payment_identifier, p.fail_reason, + i.intent_type AS "intent_type", + i.intent_payload AS "intent_payload" +FROM payments p +LEFT JOIN payment_intents i ON i.payment_id = p.id +WHERE ( + p.id > $1 OR + $1 IS NULL +) AND ( + p.id < $2 OR + $2 IS NULL +) AND ( + p.created_at >= $3 OR + $3 IS NULL +) AND ( + p.created_at <= $4 OR + $4 IS NULL +) AND ( + i.intent_type = $5 OR + $5 IS NULL OR i.intent_type IS NULL +) +ORDER BY + CASE WHEN $6 = false OR $6 IS NULL THEN p.id END ASC, + CASE WHEN $6 = true THEN p.id END DESC +LIMIT $7 +` + +type FilterPaymentsParams struct { + IndexOffsetGet sql.NullInt64 + IndexOffsetLet sql.NullInt64 + CreatedAfter sql.NullTime + CreatedBefore sql.NullTime + IntentType sql.NullInt16 + Reverse interface{} + NumLimit int32 +} + +type FilterPaymentsRow struct { + Payment Payment + IntentType sql.NullInt16 + IntentPayload []byte +} + +func (q *Queries) FilterPayments(ctx context.Context, arg FilterPaymentsParams) ([]FilterPaymentsRow, error) { + rows, err := q.db.QueryContext(ctx, filterPayments, + arg.IndexOffsetGet, + arg.IndexOffsetLet, + arg.CreatedAfter, + arg.CreatedBefore, + arg.IntentType, + arg.Reverse, + arg.NumLimit, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FilterPaymentsRow + for rows.Next() { + var i FilterPaymentsRow + if err := rows.Scan( + &i.Payment.ID, + &i.Payment.AmountMsat, + &i.Payment.CreatedAt, + &i.Payment.PaymentIdentifier, + &i.Payment.FailReason, + &i.IntentType, + &i.IntentPayload, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const insertHtlcAttempt = `-- name: InsertHtlcAttempt :one +INSERT INTO payment_htlc_attempts ( + payment_id, + attempt_index, + session_key, + attempt_time, + payment_hash, + first_hop_amount_msat, + route_total_time_lock, + route_total_amount, + route_source_key) +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9) +RETURNING id +` + +type InsertHtlcAttemptParams struct { + PaymentID int64 + AttemptIndex int64 + SessionKey []byte + AttemptTime time.Time + PaymentHash []byte + FirstHopAmountMsat int64 + RouteTotalTimeLock int32 + RouteTotalAmount int64 + RouteSourceKey []byte +} + +func (q *Queries) InsertHtlcAttempt(ctx context.Context, arg InsertHtlcAttemptParams) (int64, error) { + row := q.db.QueryRowContext(ctx, insertHtlcAttempt, + arg.PaymentID, + arg.AttemptIndex, + arg.SessionKey, + arg.AttemptTime, + arg.PaymentHash, + arg.FirstHopAmountMsat, + arg.RouteTotalTimeLock, + arg.RouteTotalAmount, + arg.RouteSourceKey, + ) + var id int64 + err := row.Scan(&id) + return id, err +} + +const insertPayment = `-- name: InsertPayment :one +INSERT INTO payments ( + amount_msat, + created_at, + payment_identifier, + fail_reason) +VALUES ( + $1, + $2, + $3, + NULL +) +RETURNING id +` + +type InsertPaymentParams struct { + AmountMsat int64 + CreatedAt time.Time + PaymentIdentifier []byte +} + +// Insert a new payment and return its ID. +// When creating a payment we don't have a fail reason because we start the +// payment process. +func (q *Queries) InsertPayment(ctx context.Context, arg InsertPaymentParams) (int64, error) { + row := q.db.QueryRowContext(ctx, insertPayment, arg.AmountMsat, arg.CreatedAt, arg.PaymentIdentifier) + var id int64 + err := row.Scan(&id) + return id, err +} + +const insertPaymentAttemptFirstHopCustomRecord = `-- name: InsertPaymentAttemptFirstHopCustomRecord :exec +INSERT INTO payment_attempt_first_hop_custom_records ( + htlc_attempt_index, + key, + value +) +VALUES ( + $1, + $2, + $3 +) +` + +type InsertPaymentAttemptFirstHopCustomRecordParams struct { + HtlcAttemptIndex int64 + Key int64 + Value []byte +} + +func (q *Queries) InsertPaymentAttemptFirstHopCustomRecord(ctx context.Context, arg InsertPaymentAttemptFirstHopCustomRecordParams) error { + _, err := q.db.ExecContext(ctx, insertPaymentAttemptFirstHopCustomRecord, arg.HtlcAttemptIndex, arg.Key, arg.Value) + return err +} + +const insertPaymentDuplicateMig = `-- name: InsertPaymentDuplicateMig :one +INSERT INTO payment_duplicates ( + payment_id, + payment_identifier, + amount_msat, + created_at, + fail_reason, + settle_preimage, + settle_time +) +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7 +) +RETURNING id +` + +type InsertPaymentDuplicateMigParams struct { + PaymentID int64 + PaymentIdentifier []byte + AmountMsat int64 + CreatedAt time.Time + FailReason sql.NullInt32 + SettlePreimage []byte + SettleTime sql.NullTime +} + +// Insert a duplicate payment record and return its ID. +func (q *Queries) InsertPaymentDuplicateMig(ctx context.Context, arg InsertPaymentDuplicateMigParams) (int64, error) { + row := q.db.QueryRowContext(ctx, insertPaymentDuplicateMig, + arg.PaymentID, + arg.PaymentIdentifier, + arg.AmountMsat, + arg.CreatedAt, + arg.FailReason, + arg.SettlePreimage, + arg.SettleTime, + ) + var id int64 + err := row.Scan(&id) + return id, err +} + +const insertPaymentFirstHopCustomRecord = `-- name: InsertPaymentFirstHopCustomRecord :exec +INSERT INTO payment_first_hop_custom_records ( + payment_id, + key, + value +) +VALUES ( + $1, + $2, + $3 +) +` + +type InsertPaymentFirstHopCustomRecordParams struct { + PaymentID int64 + Key int64 + Value []byte +} + +func (q *Queries) InsertPaymentFirstHopCustomRecord(ctx context.Context, arg InsertPaymentFirstHopCustomRecordParams) error { + _, err := q.db.ExecContext(ctx, insertPaymentFirstHopCustomRecord, arg.PaymentID, arg.Key, arg.Value) + return err +} + +const insertPaymentHopCustomRecord = `-- name: InsertPaymentHopCustomRecord :exec +INSERT INTO payment_hop_custom_records ( + hop_id, + key, + value +) +VALUES ( + $1, + $2, + $3 +) +` + +type InsertPaymentHopCustomRecordParams struct { + HopID int64 + Key int64 + Value []byte +} + +func (q *Queries) InsertPaymentHopCustomRecord(ctx context.Context, arg InsertPaymentHopCustomRecordParams) error { + _, err := q.db.ExecContext(ctx, insertPaymentHopCustomRecord, arg.HopID, arg.Key, arg.Value) + return err +} + +const insertPaymentIntent = `-- name: InsertPaymentIntent :one +INSERT INTO payment_intents ( + payment_id, + intent_type, + intent_payload) +VALUES ( + $1, + $2, + $3 +) +RETURNING id +` + +type InsertPaymentIntentParams struct { + PaymentID int64 + IntentType int16 + IntentPayload []byte +} + +// Insert a payment intent for a given payment and return its ID. +func (q *Queries) InsertPaymentIntent(ctx context.Context, arg InsertPaymentIntentParams) (int64, error) { + row := q.db.QueryRowContext(ctx, insertPaymentIntent, arg.PaymentID, arg.IntentType, arg.IntentPayload) + var id int64 + err := row.Scan(&id) + return id, err +} + +const insertPaymentMig = `-- name: InsertPaymentMig :one +/* ───────────────────────────────────────────── + Migration-specific queries + + These queries are used ONLY for the one-time migration from KV to SQL. + They are optimized for bulk historical data import, not runtime usage. + ───────────────────────────────────────────── +*/ + +INSERT INTO payments ( + amount_msat, + created_at, + payment_identifier, + fail_reason) +VALUES ( + $1, + $2, + $3, + $4 +) +RETURNING id +` + +type InsertPaymentMigParams struct { + AmountMsat int64 + CreatedAt time.Time + PaymentIdentifier []byte + FailReason sql.NullInt32 +} + +// Migration-specific payment insert that allows setting fail_reason. +// Normal InsertPayment forces fail_reason to NULL since new payments +// aren't failed yet. During migration, we're inserting historical data +// that may already be failed. +func (q *Queries) InsertPaymentMig(ctx context.Context, arg InsertPaymentMigParams) (int64, error) { + row := q.db.QueryRowContext(ctx, insertPaymentMig, + arg.AmountMsat, + arg.CreatedAt, + arg.PaymentIdentifier, + arg.FailReason, + ) + var id int64 + err := row.Scan(&id) + return id, err +} + +const insertRouteHop = `-- name: InsertRouteHop :one +INSERT INTO payment_route_hops ( + htlc_attempt_index, + hop_index, + pub_key, + scid, + outgoing_time_lock, + amt_to_forward, + meta_data +) +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7 +) +RETURNING id +` + +type InsertRouteHopParams struct { + HtlcAttemptIndex int64 + HopIndex int32 + PubKey []byte + Scid string + OutgoingTimeLock int32 + AmtToForward int64 + MetaData []byte +} + +func (q *Queries) InsertRouteHop(ctx context.Context, arg InsertRouteHopParams) (int64, error) { + row := q.db.QueryRowContext(ctx, insertRouteHop, + arg.HtlcAttemptIndex, + arg.HopIndex, + arg.PubKey, + arg.Scid, + arg.OutgoingTimeLock, + arg.AmtToForward, + arg.MetaData, + ) + var id int64 + err := row.Scan(&id) + return id, err +} + +const insertRouteHopAmp = `-- name: InsertRouteHopAmp :exec +INSERT INTO payment_route_hop_amp ( + hop_id, + root_share, + set_id, + child_index +) +VALUES ( + $1, + $2, + $3, + $4 +) +` + +type InsertRouteHopAmpParams struct { + HopID int64 + RootShare []byte + SetID []byte + ChildIndex int32 +} + +func (q *Queries) InsertRouteHopAmp(ctx context.Context, arg InsertRouteHopAmpParams) error { + _, err := q.db.ExecContext(ctx, insertRouteHopAmp, + arg.HopID, + arg.RootShare, + arg.SetID, + arg.ChildIndex, + ) + return err +} + +const insertRouteHopBlinded = `-- name: InsertRouteHopBlinded :exec +INSERT INTO payment_route_hop_blinded ( + hop_id, + encrypted_data, + blinding_point, + blinded_path_total_amt +) +VALUES ( + $1, + $2, + $3, + $4 +) +` + +type InsertRouteHopBlindedParams struct { + HopID int64 + EncryptedData []byte + BlindingPoint []byte + BlindedPathTotalAmt sql.NullInt64 +} + +func (q *Queries) InsertRouteHopBlinded(ctx context.Context, arg InsertRouteHopBlindedParams) error { + _, err := q.db.ExecContext(ctx, insertRouteHopBlinded, + arg.HopID, + arg.EncryptedData, + arg.BlindingPoint, + arg.BlindedPathTotalAmt, + ) + return err +} + +const insertRouteHopMpp = `-- name: InsertRouteHopMpp :exec +INSERT INTO payment_route_hop_mpp ( + hop_id, + payment_addr, + total_msat +) +VALUES ( + $1, + $2, + $3 +) +` + +type InsertRouteHopMppParams struct { + HopID int64 + PaymentAddr []byte + TotalMsat int64 +} + +func (q *Queries) InsertRouteHopMpp(ctx context.Context, arg InsertRouteHopMppParams) error { + _, err := q.db.ExecContext(ctx, insertRouteHopMpp, arg.HopID, arg.PaymentAddr, arg.TotalMsat) + return err +} + +const settleAttempt = `-- name: SettleAttempt :exec +INSERT INTO payment_htlc_attempt_resolutions ( + attempt_index, + resolution_time, + resolution_type, + settle_preimage +) +VALUES ( + $1, + $2, + $3, + $4 +) +` + +type SettleAttemptParams struct { + AttemptIndex int64 + ResolutionTime time.Time + ResolutionType int32 + SettlePreimage []byte +} + +func (q *Queries) SettleAttempt(ctx context.Context, arg SettleAttemptParams) error { + _, err := q.db.ExecContext(ctx, settleAttempt, + arg.AttemptIndex, + arg.ResolutionTime, + arg.ResolutionType, + arg.SettlePreimage, + ) + return err +} diff --git a/payments/db/migration1/test_harness..go b/payments/db/migration1/test_harness..go new file mode 100644 index 00000000000..b25867a4f52 --- /dev/null +++ b/payments/db/migration1/test_harness..go @@ -0,0 +1,26 @@ +package migration1 + +import ( + "testing" + + "github.com/lightningnetwork/lnd/lntypes" +) + +// TestHarness provides implementation-specific test utilities for the payments +// database. Different database backends (KV, SQL) have different internal +// structures and indexing mechanisms, so this interface allows tests to verify +// implementation-specific behavior without coupling the test logic to a +// particular backend. +type TestHarness interface { + // AssertPaymentIndex checks that a payment is correctly indexed. + // For KV: verifies the payment index bucket entry exists and points + // to the correct payment hash. + // For SQL: no-op (SQL doesn't use a separate index bucket). + AssertPaymentIndex(t *testing.T, expectedHash lntypes.Hash) + + // AssertNoIndex checks that an index for a sequence number doesn't + // exist. + // For KV: verifies the index bucket entry is deleted. + // For SQL: no-op. + AssertNoIndex(t *testing.T, seqNr uint64) +} diff --git a/payments/db/migration1/test_postgres.go b/payments/db/migration1/test_postgres.go new file mode 100644 index 00000000000..7055fb885c5 --- /dev/null +++ b/payments/db/migration1/test_postgres.go @@ -0,0 +1,94 @@ +//go:build test_db_postgres && !test_db_sqlite + +package migration1 + +import ( + "testing" + + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/payments/db/migration1/sqlc" + "github.com/lightningnetwork/lnd/sqldb" + "github.com/stretchr/testify/require" +) + +// NewTestDB is a helper function that creates a SQLStore backed by a SQL +// database for testing. +func NewTestDB(t testing.TB, opts ...OptionModifier) (DB, TestHarness) { + db := NewTestDBWithFixture(t, nil, opts...) + return db, &noopTestHarness{} +} + +// NewTestDBFixture creates a new sqldb.TestPgFixture for testing purposes. +func NewTestDBFixture(t *testing.T) *sqldb.TestPgFixture { + pgFixture := sqldb.NewTestPgFixture( + t, sqldb.DefaultPostgresFixtureLifetime, + ) + t.Cleanup(func() { + pgFixture.TearDown(t) + }) + return pgFixture +} + +// NewTestDBWithFixture is a helper function that creates a SQLStore backed by a +// SQL database for testing. +func NewTestDBWithFixture(t testing.TB, + pgFixture *sqldb.TestPgFixture, opts ...OptionModifier) DB { + + var querier BatchedSQLQueries + if pgFixture == nil { + querier = newBatchQuerier(t) + } else { + querier = newBatchQuerierWithFixture(t, pgFixture) + } + + store, err := NewSQLStore( + &SQLStoreConfig{ + QueryCfg: sqldb.DefaultPostgresConfig(), + }, querier, opts..., + ) + require.NoError(t, err) + + return store +} + +// newBatchQuerier creates a new BatchedSQLQueries instance for testing +// using a PostgreSQL database fixture. +func newBatchQuerier(t testing.TB) BatchedSQLQueries { + pgFixture := sqldb.NewTestPgFixture( + t, sqldb.DefaultPostgresFixtureLifetime, + ) + t.Cleanup(func() { + pgFixture.TearDown(t) + }) + + return newBatchQuerierWithFixture(t, pgFixture) +} + +// newBatchQuerierWithFixture creates a new BatchedSQLQueries instance for +// testing using a PostgreSQL database fixture. +func newBatchQuerierWithFixture(t testing.TB, + pgFixture *sqldb.TestPgFixture) BatchedSQLQueries { + + rawDB := sqldb.NewTestPostgresDB(t, pgFixture).BaseDB.DB + + return &testBatchedSQLQueries{ + db: rawDB, + Queries: sqlc.New(rawDB), + } +} + +// noopTestHarness is the SQL test harness implementation. Since SQL doesn't +// use a separate payment index bucket like KV, these assertions are no-ops. +type noopTestHarness struct{} + +// AssertPaymentIndex is a no-op for SQL implementations. +func (h *noopTestHarness) AssertPaymentIndex(t *testing.T, + expectedHash lntypes.Hash) { + + // No-op: SQL doesn't use a separate index bucket. +} + +// AssertNoIndex is a no-op for SQL implementations. +func (h *noopTestHarness) AssertNoIndex(t *testing.T, seqNr uint64) { + // No-op: SQL doesn't use a separate index bucket. +} diff --git a/payments/db/migration1/test_sql.go b/payments/db/migration1/test_sql.go new file mode 100644 index 00000000000..4a576fc7b64 --- /dev/null +++ b/payments/db/migration1/test_sql.go @@ -0,0 +1,59 @@ +//go:build test_db_postgres || test_db_sqlite + +package migration1 + +import ( + "context" + "database/sql" + "testing" + + "github.com/lightningnetwork/lnd/payments/db/migration1/sqlc" + "github.com/lightningnetwork/lnd/sqldb" + "github.com/stretchr/testify/require" +) + +// setupTestSQLDB creates a SQLStore-backed test database. +func setupTestSQLDB(t testing.TB, opts ...OptionModifier) *SQLStore { + t.Helper() + + db, _ := NewTestDB(t, opts...) + sqlStore, ok := db.(*SQLStore) + require.True(t, ok) + + return sqlStore +} + +// testBatchedSQLQueries is a simple implementation of BatchedSQLQueries for +// testing. +type testBatchedSQLQueries struct { + db *sql.DB + *sqlc.Queries +} + +// ExecTx implements the transaction execution logic. +func (t *testBatchedSQLQueries) ExecTx(ctx context.Context, + txOpts sqldb.TxOptions, txBody func(SQLQueries) error, + reset func()) error { + + sqlOptions := sql.TxOptions{ + Isolation: sql.LevelSerializable, + ReadOnly: txOpts.ReadOnly(), + } + + tx, err := t.db.BeginTx(ctx, &sqlOptions) + if err != nil { + return err + } + defer func() { + if err != nil { + _ = tx.Rollback() + } else { + err = tx.Commit() + } + }() + + reset() + queries := sqlc.New(tx) + + return txBody(queries) +} diff --git a/payments/db/migration1/test_sqlite.go b/payments/db/migration1/test_sqlite.go new file mode 100644 index 00000000000..b84c9c2d9ea --- /dev/null +++ b/payments/db/migration1/test_sqlite.go @@ -0,0 +1,73 @@ +//go:build !test_db_postgres && test_db_sqlite + +package migration1 + +import ( + "testing" + + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/payments/db/migration1/sqlc" + "github.com/lightningnetwork/lnd/sqldb" + "github.com/stretchr/testify/require" +) + +// NewTestDB is a helper function that creates a SQLStore backed by a SQL +// database for testing. +func NewTestDB(t testing.TB, opts ...OptionModifier) (DB, TestHarness) { + db := NewTestDBWithFixture(t, nil, opts...) + return db, &noopTestHarness{} +} + +// NewTestDBFixture is a no-op for the sqlite build. +func NewTestDBFixture(_ *testing.T) *sqldb.TestPgFixture { + return nil +} + +// NewTestDBWithFixture is a helper function that creates a SQLStore backed by a +// SQL database for testing. +func NewTestDBWithFixture(t testing.TB, _ *sqldb.TestPgFixture, + opts ...OptionModifier) DB { + + store, err := NewSQLStore( + &SQLStoreConfig{ + QueryCfg: sqldb.DefaultSQLiteConfig(), + }, newBatchQuerier(t), opts..., + ) + require.NoError(t, err) + return store +} + +// newBatchQuerier creates a new BatchedSQLQueries instance for testing +// using a SQLite database. +func newBatchQuerier(t testing.TB) BatchedSQLQueries { + return newBatchQuerierWithFixture(t, nil) +} + +// newBatchQuerierWithFixture creates a new BatchedSQLQueries instance for +// testing using a SQLite database. +func newBatchQuerierWithFixture(t testing.TB, + _ *sqldb.TestPgFixture) BatchedSQLQueries { + + rawDB := sqldb.NewTestSqliteDB(t).BaseDB.DB + + return &testBatchedSQLQueries{ + db: rawDB, + Queries: sqlc.New(rawDB), + } +} + +// noopTestHarness is the SQL test harness implementation. Since SQL doesn't +// use a separate payment index bucket like KV, these assertions are no-ops. +type noopTestHarness struct{} + +// AssertPaymentIndex is a no-op for SQL implementations. +func (h *noopTestHarness) AssertPaymentIndex(t *testing.T, + expectedHash lntypes.Hash) { + + // No-op: SQL doesn't use a separate index bucket. +} + +// AssertNoIndex is a no-op for SQL implementations. +func (h *noopTestHarness) AssertNoIndex(t *testing.T, seqNr uint64) { + // No-op: SQL doesn't use a separate index bucket. +} diff --git a/payments/db/migration1/testdata/README.md b/payments/db/migration1/testdata/README.md new file mode 100644 index 00000000000..08c5fb06b4a --- /dev/null +++ b/payments/db/migration1/testdata/README.md @@ -0,0 +1,52 @@ +# Payment Migration External Testdata + +This directory holds a real `channel.db` (bbolt) or `channel.sqlite` file for +testing the payments KV to SQL migration locally. You can also point the test +at an existing Postgres-backed kvdb instance. + +## How to use + +1. Copy your `channel.db` or `channel.sqlite` file into this folder. +2. Edit `migration_external_test.go`: + + ```go + // Comment out this line to enable the test + t.Skipf("skipping test meant for local debugging only") + + // Set to your database filename + const fileName = "channel.db" // or "channel.sqlite" + ``` + +3. Run the test: + + ```bash + # For Postgres backend + go test -v -tags="test_db_postgres" -run TestMigrationWithExternalDB + ``` + +## SQLite kvdb source + +To migrate from a `channel.sqlite` file, run with the `kvdb_sqlite` build +tag: + +```bash +go test -v -tags="test_db_sqlite kvdb_sqlite" \ + -run TestMigrationWithExternalDB +``` + +## Postgres kvdb source + +To migrate from an existing Postgres-backed kvdb instance, edit +`postgresKVDSN` in `migration_external_test.go` (set it non-empty), then +run with the `kvdb_postgres` build tag: + +```bash +go test -v -tags="kvdb_postgres test_db_postgres" \ + -run TestMigrationWithExternalDB +``` + +## Notes + +- The external database is opened read-only. +- The test creates a fresh SQL database for each run. +- Do not commit production data; keep the file local. diff --git a/payments/db/options.go b/payments/db/options.go index 9e98aafa3ac..efceb2f9b71 100644 --- a/payments/db/options.go +++ b/payments/db/options.go @@ -4,17 +4,12 @@ package paymentsdb type StoreOptions struct { // NoMigration allows to open the database in readonly mode NoMigration bool - - // KeepFailedPaymentAttempts is a flag that determines whether to keep - // failed payment attempts for a settled payment in the db. - KeepFailedPaymentAttempts bool } // DefaultOptions returns a StoreOptions populated with default values. func DefaultOptions() *StoreOptions { return &StoreOptions{ - KeepFailedPaymentAttempts: false, - NoMigration: false, + NoMigration: false, } } @@ -22,13 +17,6 @@ func DefaultOptions() *StoreOptions { // StoreOptions. type OptionModifier func(*StoreOptions) -// WithKeepFailedPaymentAttempts sets the KeepFailedPaymentAttempts to n. -func WithKeepFailedPaymentAttempts(n bool) OptionModifier { - return func(o *StoreOptions) { - o.KeepFailedPaymentAttempts = n - } -} - // WithNoMigration allows the database to be opened in read only mode by // disabling migrations. func WithNoMigration(b bool) OptionModifier { diff --git a/payments/db/payment.go b/payments/db/payment.go index 147ccdb1e77..cb2511a3419 100644 --- a/payments/db/payment.go +++ b/payments/db/payment.go @@ -359,6 +359,16 @@ type MPPayment struct { State *MPPaymentState } +// DuplicatePayment represents a legacy duplicate payment record stored +// separately from the primary payment. +type DuplicatePayment struct { + PaymentIdentifier lntypes.Hash + Amount lnwire.MilliSatoshi + CreationTime time.Time + FailureReason *FailureReason + Settle *HTLCSettleInfo +} + // Terminated returns a bool to specify whether the payment is in a terminal // state. func (m *MPPayment) Terminated() bool { @@ -744,6 +754,13 @@ func verifyAttempt(payment *MPPayment, attempt *HTLCAttemptInfo) error { // in the split payment is correct. isBlinded := len(attempt.Route.FinalHop().EncryptedData) != 0 + // For blinded payments, the last hop must set the total amount. + if isBlinded { + if attempt.Route.FinalHop().TotalAmtMsat == 0 { + return ErrBlindedPaymentMissingTotalAmount + } + } + // Make sure any existing shards match the new one with regards // to MPP options. mpp := attempt.Route.FinalHop().MPP diff --git a/payments/db/payment_test.go b/payments/db/payment_test.go index a7369c14b80..083999e9f6d 100644 --- a/payments/db/payment_test.go +++ b/payments/db/payment_test.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "math" "reflect" "testing" "time" @@ -17,6 +18,7 @@ import ( "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/record" "github.com/lightningnetwork/lnd/routing/route" + "github.com/lightningnetwork/lnd/tlv" "github.com/stretchr/testify/require" ) @@ -58,7 +60,10 @@ var ( ChannelID: 12345, OutgoingTimeLock: 111, AmtToForward: 555, - LegacyPayload: true, + + // Only tlv payloads are now supported in LND therefore we set + // LegacyPayload to false. + LegacyPayload: false, } testRoute = route.Route{ @@ -77,28 +82,37 @@ var ( SourcePubKey: vertex, Hops: []*route.Hop{ { - PubKeyBytes: vertex, - ChannelID: 9876, - OutgoingTimeLock: 120, - AmtToForward: 900, - EncryptedData: []byte{1, 3, 3}, - BlindingPoint: pub, + PubKeyBytes: vertex, + EncryptedData: []byte{1, 3, 3}, + BlindingPoint: pub, }, { PubKeyBytes: vertex, EncryptedData: []byte{3, 2, 1}, }, { + // Final hop must have AmtToForward, + // OutgoingTimeLock, and TotalAmtMsat per + // BOLT spec. We use the correct values here + // although it is not tested in this test. PubKeyBytes: vertex, - Metadata: []byte{4, 5, 6}, - AmtToForward: 500, + EncryptedData: []byte{2, 2, 2}, + AmtToForward: 1000, OutgoingTimeLock: 100, - TotalAmtMsat: 500, + TotalAmtMsat: 1000, }, }, } ) +// htlcStatus is a helper structure used in tests to track the status of an HTLC +// attempt, including whether it was settled or failed. +type htlcStatus struct { + *HTLCAttemptInfo + settle *lntypes.Preimage + failure *HTLCFailReason +} + // payment is a helper structure that holds basic information on a test payment, // such as the payment id, the status and the total number of HTLCs attempted. type payment struct { @@ -113,29 +127,36 @@ type payment struct { func createTestPayments(t *testing.T, p DB, payments []*payment) { t.Helper() + ctx := t.Context() + attemptID := uint64(0) for i := 0; i < len(payments); i++ { - info, attempt, preimg, err := genInfo(t) - require.NoError(t, err, "unable to generate htlc message") + preimg := genPreimage(t) + + rhash := sha256.Sum256(preimg[:]) + info := genPaymentCreationInfo(t, rhash) // Set the payment id accordingly in the payments slice. payments[i].id = info.PaymentIdentifier - attempt.AttemptID = attemptID + attempt := genAttemptWithHash( + t, attemptID, genSessionKey(t), rhash, + ) + attemptID++ // Init the payment. - err = p.InitPayment(info.PaymentIdentifier, info) + err := p.InitPayment(ctx, info.PaymentIdentifier, info) require.NoError(t, err, "unable to send htlc message") // Register and fail the first attempt for all payments. - _, err = p.RegisterAttempt(info.PaymentIdentifier, attempt) + _, err = p.RegisterAttempt(ctx, info.PaymentIdentifier, attempt) require.NoError(t, err, "unable to send htlc message") htlcFailure := HTLCFailUnreadable _, err = p.FailAttempt( - info.PaymentIdentifier, attempt.AttemptID, + ctx, info.PaymentIdentifier, attempt.AttemptID, &HTLCFailInfo{ Reason: htlcFailure, }, @@ -148,18 +169,20 @@ func createTestPayments(t *testing.T, p DB, payments []*payment) { // Depending on the test case, fail or succeed the next // attempt. - attempt.AttemptID = attemptID + attempt = genAttemptWithHash( + t, attemptID, genSessionKey(t), rhash, + ) attemptID++ - _, err = p.RegisterAttempt(info.PaymentIdentifier, attempt) + _, err = p.RegisterAttempt(ctx, info.PaymentIdentifier, attempt) require.NoError(t, err, "unable to send htlc message") switch payments[i].status { // Fail the attempt and the payment overall. case StatusFailed: htlcFailure := HTLCFailUnreadable - _, err = p.FailAttempt( - info.PaymentIdentifier, attempt.AttemptID, + _, err := p.FailAttempt( + ctx, info.PaymentIdentifier, attempt.AttemptID, &HTLCFailInfo{ Reason: htlcFailure, }, @@ -167,14 +190,15 @@ func createTestPayments(t *testing.T, p DB, payments []*payment) { require.NoError(t, err, "unable to fail htlc") failReason := FailureReasonNoRoute - _, err = p.Fail(info.PaymentIdentifier, - failReason) + _, err = p.Fail( + ctx, info.PaymentIdentifier, failReason, + ) require.NoError(t, err, "unable to fail payment hash") // Settle the attempt case StatusSucceeded: _, err := p.SettleAttempt( - info.PaymentIdentifier, attempt.AttemptID, + ctx, info.PaymentIdentifier, attempt.AttemptID, &HTLCSettleInfo{ Preimage: preimg, }, @@ -213,7 +237,9 @@ func assertPaymentInfo(t *testing.T, p DB, hash lntypes.Hash, t.Helper() - payment, err := p.FetchPayment(hash) + ctx := t.Context() + + payment, err := p.FetchPayment(ctx, hash) if err != nil { t.Fatal(err) } @@ -281,7 +307,9 @@ func assertDBPaymentstatus(t *testing.T, p DB, hash lntypes.Hash, t.Helper() - payment, err := p.FetchPayment(hash) + ctx := t.Context() + + payment, err := p.FetchPayment(ctx, hash) if errors.Is(err, ErrPaymentNotInitiated) { return } @@ -334,42 +362,90 @@ func assertDBPayments(t *testing.T, paymentDB DB, payments []*payment) { } // genPreimage generates a random preimage. -func genPreimage(t *testing.T) ([32]byte, error) { +func genPreimage(t *testing.T) lntypes.Preimage { t.Helper() var preimage [32]byte - if _, err := io.ReadFull(rand.Reader, preimage[:]); err != nil { - return preimage, err - } + _, err := io.ReadFull(rand.Reader, preimage[:]) + require.NoError(t, err, "unable to generate preimage") - return preimage, nil + return preimage } -// genInfo generates a payment creation info, an attempt info and a preimage. -func genInfo(t *testing.T) (*PaymentCreationInfo, *HTLCAttemptInfo, - lntypes.Preimage, error) { +// genSessionKey generates a new random private key for use as a session key. +func genSessionKey(t *testing.T) *btcec.PrivateKey { + t.Helper() - preimage, err := genPreimage(t) - if err != nil { - return nil, nil, preimage, fmt.Errorf("unable to "+ - "generate preimage: %v", err) + key, err := btcec.NewPrivateKey() + require.NoError(t, err) + + return key +} + +// genPaymentCreationInfo generates a payment creation info. +func genPaymentCreationInfo(t *testing.T, + paymentHash lntypes.Hash) *PaymentCreationInfo { + + t.Helper() + + // Add constant first hop custom records for testing for testing + // purposes. + firstHopCustomRecords := lnwire.CustomRecords{ + lnwire.MinCustomRecordsTlvType + 1: []byte("test_record_1"), + lnwire.MinCustomRecordsTlvType + 2: []byte("test_record_2"), + lnwire.MinCustomRecordsTlvType + 3: []byte{ + 0x01, 0x02, 0x03, 0x04, 0x05, + }, } + return &PaymentCreationInfo{ + PaymentIdentifier: paymentHash, + Value: testRoute.ReceiverAmt(), + CreationTime: time.Unix(time.Now().Unix(), 0), + PaymentRequest: []byte("hola"), + FirstHopCustomRecords: firstHopCustomRecords, + } +} + +// genPreimageAndHash generates a random preimage and its corresponding hash. +func genPreimageAndHash(t *testing.T) (lntypes.Preimage, lntypes.Hash) { + t.Helper() + + preimage := genPreimage(t) + rhash := sha256.Sum256(preimage[:]) var hash lntypes.Hash copy(hash[:], rhash[:]) + return preimage, hash +} + +// genAttemptWithPreimage generates an HTLC attempt and returns both the +// attempt and preimage. +func genAttemptWithHash(t *testing.T, attemptID uint64, + sessionKey *btcec.PrivateKey, hash lntypes.Hash) *HTLCAttemptInfo { + + t.Helper() + attempt, err := NewHtlcAttempt( - 0, priv, *testRoute.Copy(), time.Time{}, &hash, + attemptID, sessionKey, *testRoute.Copy(), time.Time{}, + &hash, ) - require.NoError(t, err) + require.NoError(t, err, "unable to generate htlc attempt") - return &PaymentCreationInfo{ - PaymentIdentifier: rhash, - Value: testRoute.ReceiverAmt(), - CreationTime: time.Unix(time.Now().Unix(), 0), - PaymentRequest: []byte("hola"), - }, &attempt.HTLCAttemptInfo, preimage, nil + return &attempt.HTLCAttemptInfo +} + +// genInfo generates a payment creation info and the corresponding preimage. +func genInfo(t *testing.T) (*PaymentCreationInfo, lntypes.Preimage) { + t.Helper() + + preimage, _ := genPreimageAndHash(t) + + rhash := sha256.Sum256(preimage[:]) + creationInfo := genPaymentCreationInfo(t, rhash) + + return creationInfo, preimage } // TestDeleteFailedAttempts checks that DeleteFailedAttempts properly removes @@ -377,20 +453,7 @@ func genInfo(t *testing.T) (*PaymentCreationInfo, *HTLCAttemptInfo, func TestDeleteFailedAttempts(t *testing.T) { t.Parallel() - t.Run("keep failed payment attempts", func(t *testing.T) { - testDeleteFailedAttempts(t, true) - }) - t.Run("remove failed payment attempts", func(t *testing.T) { - testDeleteFailedAttempts(t, false) - }) -} - -// testDeleteFailedAttempts tests the DeleteFailedAttempts method with the -// given keepFailedPaymentAttempts flag as argument. -func testDeleteFailedAttempts(t *testing.T, keepFailedPaymentAttempts bool) { - paymentDB := NewTestDB( - t, WithKeepFailedPaymentAttempts(keepFailedPaymentAttempts), - ) + paymentDB, _ := NewTestDB(t) // Register three payments: // All payments will have one failed HTLC attempt and one HTLC attempt @@ -420,72 +483,63 @@ func testDeleteFailedAttempts(t *testing.T, keepFailedPaymentAttempts bool) { // Calling DeleteFailedAttempts on a failed payment should delete all // HTLCs. - require.NoError(t, paymentDB.DeleteFailedAttempts(payments[0].id)) + require.NoError(t, paymentDB.DeleteFailedAttempts( + t.Context(), payments[0].id, + )) - // Expect all HTLCs to be deleted if the config is set to delete them. - if !keepFailedPaymentAttempts { - payments[0].htlcs = 0 - } + // Expect all HTLCs to be deleted. + payments[0].htlcs = 0 assertDBPayments(t, paymentDB, payments) // Calling DeleteFailedAttempts on an in-flight payment should return // an error. - // - // NOTE: In case the option keepFailedPaymentAttempts is set no delete - // operation are performed in general therefore we do NOT expect an - // error in this case. - if keepFailedPaymentAttempts { - require.NoError( - t, paymentDB.DeleteFailedAttempts(payments[1].id), - ) - } else { - require.Error(t, paymentDB.DeleteFailedAttempts(payments[1].id)) - } + err := paymentDB.DeleteFailedAttempts( + t.Context(), payments[1].id, + ) + require.Error(t, err) // Since DeleteFailedAttempts returned an error, we should expect the // payment to be unchanged. assertDBPayments(t, paymentDB, payments) // Cleaning up a successful payment should remove failed htlcs. - require.NoError(t, paymentDB.DeleteFailedAttempts(payments[2].id)) + require.NoError(t, paymentDB.DeleteFailedAttempts( + t.Context(), payments[2].id, + )) - // Expect all HTLCs except for the settled one to be deleted if the - // config is set to delete them. - if !keepFailedPaymentAttempts { - payments[2].htlcs = 1 - } + // Expect all HTLCs except for the settled one to be deleted. + payments[2].htlcs = 1 assertDBPayments(t, paymentDB, payments) - // NOTE: In case the option keepFailedPaymentAttempts is set no delete - // operation are performed in general therefore we do NOT expect an - // error in this case. - if keepFailedPaymentAttempts { - // DeleteFailedAttempts is ignored, even for non-existent - // payments, if the control tower is configured to keep failed - // HTLCs. - require.NoError( - t, paymentDB.DeleteFailedAttempts(lntypes.ZeroHash), - ) - } else { - // Attempting to cleanup a non-existent payment returns an - // error. - require.Error( - t, paymentDB.DeleteFailedAttempts(lntypes.ZeroHash), - ) - } + // Attempting to cleanup a non-existent payment returns an error. + require.Error( + t, paymentDB.DeleteFailedAttempts( + t.Context(), lntypes.ZeroHash, + ), + ) } // TestMPPRecordValidation tests MPP record validation. func TestMPPRecordValidation(t *testing.T) { t.Parallel() - paymentDB := NewTestDB(t) + ctx := t.Context() + + paymentDB, _ := NewTestDB(t) + + preimg := genPreimage(t) - info, attempt, _, err := genInfo(t) - require.NoError(t, err, "unable to generate htlc message") + rhash := sha256.Sum256(preimg[:]) + info := genPaymentCreationInfo(t, rhash) + + attemptID := uint64(0) + + attempt := genAttemptWithHash( + t, attemptID, genSessionKey(t), rhash, + ) // Init the payment. - err = paymentDB.InitPayment(info.PaymentIdentifier, info) + err := paymentDB.InitPayment(ctx, info.PaymentIdentifier, info) require.NoError(t, err, "unable to send htlc message") // Create three unique attempts we'll use for the test, and @@ -498,50 +552,74 @@ func TestMPPRecordValidation(t *testing.T) { info.Value, [32]byte{1}, ) - _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, attempt) + _, err = paymentDB.RegisterAttempt(ctx, info.PaymentIdentifier, attempt) require.NoError(t, err, "unable to send htlc message") // Now try to register a non-MPP attempt, which should fail. - b := *attempt - b.AttemptID = 1 - b.Route.FinalHop().MPP = nil - _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, &b) + attemptID++ + attempt2 := genAttemptWithHash( + t, attemptID, genSessionKey(t), rhash, + ) + + attempt2.Route.FinalHop().MPP = nil + + _, err = paymentDB.RegisterAttempt( + ctx, info.PaymentIdentifier, attempt2, + ) require.ErrorIs(t, err, ErrMPPayment) // Try to register attempt one with a different payment address. - b.Route.FinalHop().MPP = record.NewMPP( + attempt2.Route.FinalHop().MPP = record.NewMPP( info.Value, [32]byte{2}, ) - _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, &b) + _, err = paymentDB.RegisterAttempt( + ctx, info.PaymentIdentifier, attempt2, + ) require.ErrorIs(t, err, ErrMPPPaymentAddrMismatch) // Try registering one with a different total amount. - b.Route.FinalHop().MPP = record.NewMPP( + attempt2.Route.FinalHop().MPP = record.NewMPP( info.Value/2, [32]byte{1}, ) - _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, &b) + _, err = paymentDB.RegisterAttempt( + ctx, info.PaymentIdentifier, attempt2, + ) require.ErrorIs(t, err, ErrMPPTotalAmountMismatch) // Create and init a new payment. This time we'll check that we cannot // register an MPP attempt if we already registered a non-MPP one. - info, attempt, _, err = genInfo(t) - require.NoError(t, err, "unable to generate htlc message") + preimg = genPreimage(t) + + rhash = sha256.Sum256(preimg[:]) + info = genPaymentCreationInfo(t, rhash) + + attemptID++ + attempt = genAttemptWithHash( + t, attemptID, genSessionKey(t), rhash, + ) - err = paymentDB.InitPayment(info.PaymentIdentifier, info) + err = paymentDB.InitPayment(ctx, info.PaymentIdentifier, info) require.NoError(t, err, "unable to send htlc message") attempt.Route.FinalHop().MPP = nil - _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, attempt) + _, err = paymentDB.RegisterAttempt( + ctx, info.PaymentIdentifier, attempt, + ) require.NoError(t, err, "unable to send htlc message") // Attempt to register an MPP attempt, which should fail. - b = *attempt - b.AttemptID = 1 - b.Route.FinalHop().MPP = record.NewMPP( + attemptID++ + attempt2 = genAttemptWithHash( + t, attemptID, genSessionKey(t), rhash, + ) + + attempt2.Route.FinalHop().MPP = record.NewMPP( info.Value, [32]byte{1}, ) - _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, &b) + _, err = paymentDB.RegisterAttempt( + ctx, info.PaymentIdentifier, attempt2, + ) require.ErrorIs(t, err, ErrNonMPPayment) } @@ -550,7 +628,9 @@ func TestMPPRecordValidation(t *testing.T) { func TestDeleteSinglePayment(t *testing.T) { t.Parallel() - paymentDB := NewTestDB(t) + ctx := t.Context() + + paymentDB, _ := NewTestDB(t) // Register four payments: // All payments will have one failed HTLC attempt and one HTLC attempt @@ -581,7 +661,9 @@ func TestDeleteSinglePayment(t *testing.T) { assertDBPayments(t, paymentDB, payments) // Delete HTLC attempts for first payment only. - require.NoError(t, paymentDB.DeletePayment(payments[0].id, true)) + require.NoError(t, paymentDB.DeletePayment( + ctx, payments[0].id, true, + )) // The first payment is the only altered one as its failed HTLC should // have been removed but is still present as payment. @@ -589,19 +671,25 @@ func TestDeleteSinglePayment(t *testing.T) { assertDBPayments(t, paymentDB, payments) // Delete the first payment completely. - require.NoError(t, paymentDB.DeletePayment(payments[0].id, false)) + require.NoError(t, paymentDB.DeletePayment( + ctx, payments[0].id, false, + )) // The first payment should have been deleted. assertDBPayments(t, paymentDB, payments[1:]) // Now delete the second payment completely. - require.NoError(t, paymentDB.DeletePayment(payments[1].id, false)) + require.NoError(t, paymentDB.DeletePayment( + ctx, payments[1].id, false, + )) // The Second payment should have been deleted. assertDBPayments(t, paymentDB, payments[2:]) // Delete failed HTLC attempts for the third payment. - require.NoError(t, paymentDB.DeletePayment(payments[2].id, true)) + require.NoError(t, paymentDB.DeletePayment( + ctx, payments[2].id, true, + )) // Only the successful HTLC attempt should be left for the third // payment. @@ -609,21 +697,27 @@ func TestDeleteSinglePayment(t *testing.T) { assertDBPayments(t, paymentDB, payments[2:]) // Now delete the third payment completely. - require.NoError(t, paymentDB.DeletePayment(payments[2].id, false)) + require.NoError(t, paymentDB.DeletePayment( + ctx, payments[2].id, false, + )) // Only the last payment should be left. assertDBPayments(t, paymentDB, payments[3:]) // Deleting HTLC attempts from InFlight payments should not work and an // error returned. - require.Error(t, paymentDB.DeletePayment(payments[3].id, true)) + require.Error(t, paymentDB.DeletePayment( + ctx, payments[3].id, true, + )) // The payment is InFlight and therefore should not have been altered. assertDBPayments(t, paymentDB, payments[3:]) // Finally deleting the InFlight payment should also not work and an // error returned. - require.Error(t, paymentDB.DeletePayment(payments[3].id, false)) + require.Error(t, paymentDB.DeletePayment( + ctx, payments[3].id, false, + )) // The payment is InFlight and therefore should not have been altered. assertDBPayments(t, paymentDB, payments[3:]) @@ -1388,6 +1482,45 @@ func TestVerifyAttemptBlindedValidation(t *testing.T) { require.NoError(t, verifyAttempt(payment, &matching)) } +// TestVerifyAttemptBlindedMissingTotalAmount tests that we return an error if +// we try to register a blinded payment attempt where the final hop doesn't set +// the total amount. +func TestVerifyAttemptBlindedMissingTotalAmount(t *testing.T) { + t.Parallel() + + total := lnwire.MilliSatoshi(5000) + + // Payment with no existing attempts. + payment := makePayment(total) + + // Attempt with encrypted data (blinded payment) but missing total + // amount. + attemptMissingTotal := makeLastHopAttemptInfo( + 1, + lastHopArgs{ + amt: 2500, + total: 0, + encrypted: []byte{1, 2, 3}, + }, + ) + require.ErrorIs( + t, + verifyAttempt(payment, &attemptMissingTotal), + ErrBlindedPaymentMissingTotalAmount, + ) + + // Attempt with encrypted data and valid total amount should succeed. + attemptWithTotal := makeLastHopAttemptInfo( + 2, + lastHopArgs{ + amt: 2500, + total: total, + encrypted: []byte{4, 5, 6}, + }, + ) + require.NoError(t, verifyAttempt(payment, &attemptWithTotal)) +} + // TestVerifyAttemptBlindedMixedWithNonBlinded tests that we return an error if // we try to register a non-MPP attempt for a blinded payment. func TestVerifyAttemptBlindedMixedWithNonBlinded(t *testing.T) { @@ -1454,13 +1587,16 @@ func TestEmptyRoutesGenerateSphinxPacket(t *testing.T) { func TestSuccessesWithoutInFlight(t *testing.T) { t.Parallel() - paymentDB := NewTestDB(t) + paymentDB, _ := NewTestDB(t) - info, _, preimg, err := genInfo(t) - require.NoError(t, err, "unable to generate htlc message") + preimg := genPreimage(t) + + rhash := sha256.Sum256(preimg[:]) + info := genPaymentCreationInfo(t, rhash) // Attempt to complete the payment should fail. - _, err = paymentDB.SettleAttempt( + _, err := paymentDB.SettleAttempt( + t.Context(), info.PaymentIdentifier, 0, &HTLCSettleInfo{ Preimage: preimg, @@ -1474,14 +1610,16 @@ func TestSuccessesWithoutInFlight(t *testing.T) { func TestFailsWithoutInFlight(t *testing.T) { t.Parallel() - paymentDB := NewTestDB(t) + paymentDB, _ := NewTestDB(t) - info, _, _, err := genInfo(t) - require.NoError(t, err, "unable to generate htlc message") + preimg := genPreimage(t) + + rhash := sha256.Sum256(preimg[:]) + info := genPaymentCreationInfo(t, rhash) // Calling Fail should return an error. - _, err = paymentDB.Fail( - info.PaymentIdentifier, FailureReasonNoRoute, + _, err := paymentDB.Fail( + t.Context(), info.PaymentIdentifier, FailureReasonNoRoute, ) require.ErrorIs(t, err, ErrPaymentNotInitiated) } @@ -1491,7 +1629,9 @@ func TestFailsWithoutInFlight(t *testing.T) { func TestDeletePayments(t *testing.T) { t.Parallel() - paymentDB := NewTestDB(t) + ctx := t.Context() + + paymentDB, _ := NewTestDB(t) // Register three payments: // 1. A payment with two failed attempts. @@ -1511,7 +1651,7 @@ func TestDeletePayments(t *testing.T) { assertDBPayments(t, paymentDB, payments) // Delete HTLC attempts for failed payments only. - numPayments, err := paymentDB.DeletePayments(true, true) + numPayments, err := paymentDB.DeletePayments(ctx, true, true) require.NoError(t, err) require.EqualValues(t, 0, numPayments) @@ -1520,7 +1660,7 @@ func TestDeletePayments(t *testing.T) { assertDBPayments(t, paymentDB, payments) // Delete failed attempts for all payments. - numPayments, err = paymentDB.DeletePayments(false, true) + numPayments, err = paymentDB.DeletePayments(ctx, false, true) require.NoError(t, err) require.EqualValues(t, 0, numPayments) @@ -1530,36 +1670,201 @@ func TestDeletePayments(t *testing.T) { assertDBPayments(t, paymentDB, payments) // Now delete all failed payments. - numPayments, err = paymentDB.DeletePayments(true, false) + numPayments, err = paymentDB.DeletePayments(ctx, true, false) require.NoError(t, err) require.EqualValues(t, 1, numPayments) assertDBPayments(t, paymentDB, payments[1:]) // Finally delete all completed payments. - numPayments, err = paymentDB.DeletePayments(false, false) + numPayments, err = paymentDB.DeletePayments(ctx, false, false) require.NoError(t, err) require.EqualValues(t, 1, numPayments) assertDBPayments(t, paymentDB, payments[2:]) } +// TestDeleteNonInFlight checks that calling DeletePayments only deletes +// payments from the database that are not in-flight. +func TestDeleteNonInFlight(t *testing.T) { + t.Parallel() + + ctx := t.Context() + + paymentDB, _ := NewTestDB(t) + + var ( + numSuccess, numInflight int + attemptID uint64 = 0 + ) + + // Create payments with different statuses: failed, success, inflight, + // and another success. + payments := []struct { + failed bool + success bool + }{ + // Payment 0: failed. + {failed: true, success: false}, + // Payment 1: success. + {failed: false, success: true}, + // Payment 2: inflight. + {failed: false, success: false}, + // Payment 3: success. + {failed: false, success: true}, + } + + for _, p := range payments { + preimg := genPreimage(t) + + rhash := sha256.Sum256(preimg[:]) + info := genPaymentCreationInfo(t, rhash) + attempt := genAttemptWithHash( + t, attemptID, genSessionKey(t), rhash, + ) + + // After generating the attempt, increment the attempt ID to + // have unique attempt IDs for each attempt otherwise the unique + // constraint on the attempt ID will be violated. + attemptID++ + + // Init payment which initiates StatusInFlight. + err := paymentDB.InitPayment(ctx, info.PaymentIdentifier, info) + require.NoError(t, err, "unable to init payment") + + _, err = paymentDB.RegisterAttempt( + ctx, info.PaymentIdentifier, attempt, + ) + require.NoError(t, err, "unable to register attempt") + + switch { + case p.failed: + // Fail the payment attempt. + htlcFailure := HTLCFailUnreadable + _, err := paymentDB.FailAttempt( + ctx, info.PaymentIdentifier, attempt.AttemptID, + &HTLCFailInfo{ + Reason: htlcFailure, + }, + ) + require.NoError(t, err, "unable to fail htlc") + + // Fail the payment, which should move it to Failed. + failReason := FailureReasonNoRoute + _, err = paymentDB.Fail( + ctx, info.PaymentIdentifier, failReason, + ) + require.NoError(t, err, "unable to fail payment") + + // Verify the status is indeed Failed. + assertDBPaymentstatus( + t, paymentDB, info.PaymentIdentifier, + StatusFailed, + ) + + case p.success: + // Settle the attempt. + _, err := paymentDB.SettleAttempt( + ctx, info.PaymentIdentifier, attempt.AttemptID, + &HTLCSettleInfo{ + Preimage: preimg, + }, + ) + require.NoError(t, err, "unable to settle attempt") + + assertDBPaymentstatus( + t, paymentDB, info.PaymentIdentifier, + StatusSucceeded, + ) + + numSuccess++ + + default: + // Leave as inflight. + assertDBPaymentstatus( + t, paymentDB, info.PaymentIdentifier, + StatusInFlight, + ) + + numInflight++ + } + } + + // Delete all failed payments. + numPayments, err := paymentDB.DeletePayments(ctx, true, false) + require.NoError(t, err) + require.EqualValues(t, 1, numPayments) + + // This should leave the succeeded and in-flight payments. + resp, err := paymentDB.QueryPayments(ctx, Query{ + IndexOffset: 0, + MaxPayments: math.MaxUint64, + IncludeIncomplete: true, + }) + require.NoError(t, err) + + require.Equal(t, numSuccess+numInflight, len(resp.Payments), + "expected %d payments, got %d", numSuccess+numInflight, + len(resp.Payments)) + + var s, i int + for _, p := range resp.Payments { + switch p.Status { + case StatusSucceeded: + s++ + case StatusInFlight: + i++ + } + } + + require.Equal(t, numSuccess, s, + "expected %d succeeded payments, got %d", numSuccess, s) + require.Equal(t, numInflight, i, + "expected %d in-flight payments, got %d", numInflight, i) + + // Now delete all payments except in-flight. + numPayments, err = paymentDB.DeletePayments(ctx, false, false) + require.NoError(t, err) + require.EqualValues(t, 2, numPayments) + + // This should leave the in-flight payment. + resp, err = paymentDB.QueryPayments(ctx, Query{ + IndexOffset: 0, + MaxPayments: math.MaxUint64, + IncludeIncomplete: true, + }) + require.NoError(t, err) + + require.Equal(t, numInflight, len(resp.Payments), + "expected %d payments, got %d", numInflight, len(resp.Payments)) + + for _, p := range resp.Payments { + require.Equal(t, StatusInFlight, p.Status, + "expected in-flight status, got %v", p.Status) + } +} + // TestSwitchDoubleSend checks the ability of payment control to // prevent double sending of htlc message, when message is in StatusInFlight. func TestSwitchDoubleSend(t *testing.T) { t.Parallel() - paymentDB := NewTestDB(t) + ctx := t.Context() + + paymentDB, harness := NewTestDB(t) - info, attempt, preimg, err := genInfo(t) - require.NoError(t, err, "unable to generate htlc message") + preimg := genPreimage(t) + + rhash := sha256.Sum256(preimg[:]) + info := genPaymentCreationInfo(t, rhash) + attempt := genAttemptWithHash(t, 0, genSessionKey(t), rhash) // Sends base htlc message which initiate base status and move it to // StatusInFlight and verifies that it was changed. - err = paymentDB.InitPayment(info.PaymentIdentifier, info) + err := paymentDB.InitPayment(ctx, info.PaymentIdentifier, info) require.NoError(t, err, "unable to send htlc message") - assertPaymentIndex(t, paymentDB, info.PaymentIdentifier) + harness.AssertPaymentIndex(t, info.PaymentIdentifier) assertDBPaymentstatus( t, paymentDB, info.PaymentIdentifier, StatusInitiated, ) @@ -1570,11 +1875,11 @@ func TestSwitchDoubleSend(t *testing.T) { // Try to initiate double sending of htlc message with the same // payment hash, should result in error indicating that payment has // already been sent. - err = paymentDB.InitPayment(info.PaymentIdentifier, info) + err = paymentDB.InitPayment(ctx, info.PaymentIdentifier, info) require.ErrorIs(t, err, ErrPaymentExists) // Record an attempt. - _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, attempt) + _, err = paymentDB.RegisterAttempt(ctx, info.PaymentIdentifier, attempt) require.NoError(t, err, "unable to send htlc message") assertDBPaymentstatus( t, paymentDB, info.PaymentIdentifier, StatusInFlight, @@ -1588,7 +1893,7 @@ func TestSwitchDoubleSend(t *testing.T) { ) // Sends base htlc message which initiate StatusInFlight. - err = paymentDB.InitPayment(info.PaymentIdentifier, info) + err = paymentDB.InitPayment(ctx, info.PaymentIdentifier, info) if !errors.Is(err, ErrPaymentInFlight) { t.Fatalf("payment control wrong behaviour: " + "double sending must trigger ErrPaymentInFlight error") @@ -1596,7 +1901,7 @@ func TestSwitchDoubleSend(t *testing.T) { // After settling, the error should be ErrAlreadyPaid. _, err = paymentDB.SettleAttempt( - info.PaymentIdentifier, attempt.AttemptID, + ctx, info.PaymentIdentifier, attempt.AttemptID, &HTLCSettleInfo{ Preimage: preimg, }, @@ -1611,7 +1916,7 @@ func TestSwitchDoubleSend(t *testing.T) { t, paymentDB, info.PaymentIdentifier, info, nil, htlc, ) - err = paymentDB.InitPayment(info.PaymentIdentifier, info) + err = paymentDB.InitPayment(ctx, info.PaymentIdentifier, info) if !errors.Is(err, ErrAlreadyPaid) { t.Fatalf("unable to send htlc message: %v", err) } @@ -1622,16 +1927,21 @@ func TestSwitchDoubleSend(t *testing.T) { func TestSwitchFail(t *testing.T) { t.Parallel() - paymentDB := NewTestDB(t) + ctx := t.Context() + + paymentDB, harness := NewTestDB(t) - info, attempt, preimg, err := genInfo(t) - require.NoError(t, err, "unable to generate htlc message") + preimg := genPreimage(t) + + rhash := sha256.Sum256(preimg[:]) + info := genPaymentCreationInfo(t, rhash) + attempt := genAttemptWithHash(t, 0, genSessionKey(t), rhash) // Sends base htlc message which initiate StatusInFlight. - err = paymentDB.InitPayment(info.PaymentIdentifier, info) + err := paymentDB.InitPayment(ctx, info.PaymentIdentifier, info) require.NoError(t, err, "unable to send htlc message") - assertPaymentIndex(t, paymentDB, info.PaymentIdentifier) + harness.AssertPaymentIndex(t, info.PaymentIdentifier) assertDBPaymentstatus( t, paymentDB, info.PaymentIdentifier, StatusInitiated, ) @@ -1641,7 +1951,7 @@ func TestSwitchFail(t *testing.T) { // Fail the payment, which should moved it to Failed. failReason := FailureReasonNoRoute - _, err = paymentDB.Fail(info.PaymentIdentifier, failReason) + _, err = paymentDB.Fail(ctx, info.PaymentIdentifier, failReason) require.NoError(t, err, "unable to fail payment hash") // Verify the status is indeed Failed. @@ -1655,18 +1965,18 @@ func TestSwitchFail(t *testing.T) { // Lookup the payment so we can get its old sequence number before it is // overwritten. - payment, err := paymentDB.FetchPayment(info.PaymentIdentifier) + payment, err := paymentDB.FetchPayment(ctx, info.PaymentIdentifier) require.NoError(t, err) // Sends the htlc again, which should succeed since the prior payment // failed. - err = paymentDB.InitPayment(info.PaymentIdentifier, info) + err = paymentDB.InitPayment(ctx, info.PaymentIdentifier, info) require.NoError(t, err, "unable to send htlc message") // Check that our index has been updated, and the old index has been // removed. - assertPaymentIndex(t, paymentDB, info.PaymentIdentifier) - assertNoIndex(t, paymentDB, payment.SequenceNum) + harness.AssertPaymentIndex(t, info.PaymentIdentifier) + harness.AssertNoIndex(t, payment.SequenceNum) assertDBPaymentstatus( t, paymentDB, info.PaymentIdentifier, StatusInitiated, @@ -1678,12 +1988,12 @@ func TestSwitchFail(t *testing.T) { // Record a new attempt. In this test scenario, the attempt fails. // However, this is not communicated to control tower in the current // implementation. It only registers the initiation of the attempt. - _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, attempt) + _, err = paymentDB.RegisterAttempt(ctx, info.PaymentIdentifier, attempt) require.NoError(t, err, "unable to register attempt") htlcReason := HTLCFailUnreadable _, err = paymentDB.FailAttempt( - info.PaymentIdentifier, attempt.AttemptID, + ctx, info.PaymentIdentifier, attempt.AttemptID, &HTLCFailInfo{ Reason: htlcReason, }, @@ -1703,8 +2013,12 @@ func TestSwitchFail(t *testing.T) { assertPaymentInfo(t, paymentDB, info.PaymentIdentifier, info, nil, htlc) // Record another attempt. - attempt.AttemptID = 1 - _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, attempt) + attempt = genAttemptWithHash( + t, 1, genSessionKey(t), rhash, + ) + require.NoError(t, err) + + _, err = paymentDB.RegisterAttempt(ctx, info.PaymentIdentifier, attempt) require.NoError(t, err, "unable to send htlc message") assertDBPaymentstatus( t, paymentDB, info.PaymentIdentifier, StatusInFlight, @@ -1721,7 +2035,7 @@ func TestSwitchFail(t *testing.T) { // Settle the attempt and verify that status was changed to // StatusSucceeded. payment, err = paymentDB.SettleAttempt( - info.PaymentIdentifier, attempt.AttemptID, + ctx, info.PaymentIdentifier, attempt.AttemptID, &HTLCSettleInfo{ Preimage: preimg, }, @@ -1751,7 +2065,7 @@ func TestSwitchFail(t *testing.T) { // Attempt a final payment, which should now fail since the prior // payment succeed. - err = paymentDB.InitPayment(info.PaymentIdentifier, info) + err = paymentDB.InitPayment(ctx, info.PaymentIdentifier, info) if !errors.Is(err, ErrAlreadyPaid) { t.Fatalf("unable to send htlc message: %v", err) } @@ -1762,6 +2076,8 @@ func TestSwitchFail(t *testing.T) { func TestMultiShard(t *testing.T) { t.Parallel() + ctx := t.Context() + // We will register three HTLC attempts, and always fail the second // one. We'll generate all combinations of settling/failing the first // and third HTLC, and assert that the payment status end up as we @@ -1779,20 +2095,18 @@ func TestMultiShard(t *testing.T) { } runSubTest := func(t *testing.T, test testCase) { - paymentDB := NewTestDB(t) + paymentDB, harness := NewTestDB(t) - info, attempt, preimg, err := genInfo(t) - if err != nil { - t.Fatalf("unable to generate htlc message: %v", err) - } + preimg := genPreimage(t) + + rhash := sha256.Sum256(preimg[:]) + info := genPaymentCreationInfo(t, rhash) // Init the payment, moving it to the StatusInFlight state. - err = paymentDB.InitPayment(info.PaymentIdentifier, info) - if err != nil { - t.Fatalf("unable to send htlc message: %v", err) - } + err := paymentDB.InitPayment(ctx, info.PaymentIdentifier, info) + require.NoError(t, err) - assertPaymentIndex(t, paymentDB, info.PaymentIdentifier) + harness.AssertPaymentIndex(t, info.PaymentIdentifier) assertDBPaymentstatus( t, paymentDB, info.PaymentIdentifier, StatusInitiated, ) @@ -1805,19 +2119,22 @@ func TestMultiShard(t *testing.T) { // attempts's value to one third of the payment amount, and // populate the MPP options. shardAmt := info.Value / 3 - attempt.Route.FinalHop().AmtToForward = shardAmt - attempt.Route.FinalHop().MPP = record.NewMPP( - info.Value, [32]byte{1}, - ) var attempts []*HTLCAttemptInfo for i := uint64(0); i < 3; i++ { - a := *attempt - a.AttemptID = i - attempts = append(attempts, &a) + a := genAttemptWithHash( + t, i, genSessionKey(t), rhash, + ) + + a.Route.FinalHop().AmtToForward = shardAmt + a.Route.FinalHop().MPP = record.NewMPP( + info.Value, [32]byte{1}, + ) + + attempts = append(attempts, a) _, err = paymentDB.RegisterAttempt( - info.PaymentIdentifier, &a, + ctx, info.PaymentIdentifier, a, ) if err != nil { t.Fatalf("unable to send htlc message: %v", err) @@ -1828,7 +2145,7 @@ func TestMultiShard(t *testing.T) { ) htlc := &htlcStatus{ - HTLCAttemptInfo: &a, + HTLCAttemptInfo: a, } assertPaymentInfo( t, paymentDB, info.PaymentIdentifier, info, nil, @@ -1839,16 +2156,25 @@ func TestMultiShard(t *testing.T) { // For a fourth attempt, check that attempting to // register it will fail since the total sent amount // will be too large. - b := *attempt - b.AttemptID = 3 - _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, &b) + b := genAttemptWithHash( + t, 3, genSessionKey(t), rhash, + ) + + b.Route.FinalHop().AmtToForward = shardAmt + b.Route.FinalHop().MPP = record.NewMPP( + info.Value, [32]byte{1}, + ) + + _, err = paymentDB.RegisterAttempt( + ctx, info.PaymentIdentifier, b, + ) require.ErrorIs(t, err, ErrValueExceedsAmt) // Fail the second attempt. a := attempts[1] htlcFail := HTLCFailUnreadable _, err = paymentDB.FailAttempt( - info.PaymentIdentifier, a.AttemptID, + ctx, info.PaymentIdentifier, a.AttemptID, &HTLCFailInfo{ Reason: htlcFail, }, @@ -1879,7 +2205,7 @@ func TestMultiShard(t *testing.T) { var firstFailReason *FailureReason if test.settleFirst { _, err := paymentDB.SettleAttempt( - info.PaymentIdentifier, a.AttemptID, + ctx, info.PaymentIdentifier, a.AttemptID, &HTLCSettleInfo{ Preimage: preimg, }, @@ -1897,7 +2223,7 @@ func TestMultiShard(t *testing.T) { ) } else { _, err := paymentDB.FailAttempt( - info.PaymentIdentifier, a.AttemptID, + ctx, info.PaymentIdentifier, a.AttemptID, &HTLCFailInfo{ Reason: htlcFail, }, @@ -1918,7 +2244,7 @@ func TestMultiShard(t *testing.T) { // a terminal state. failReason := FailureReasonNoRoute _, err = paymentDB.Fail( - info.PaymentIdentifier, failReason, + ctx, info.PaymentIdentifier, failReason, ) if err != nil { t.Fatalf("unable to fail payment hash: %v", err) @@ -1938,9 +2264,18 @@ func TestMultiShard(t *testing.T) { // Try to register yet another attempt. This should fail now // that the payment has reached a terminal condition. - b = *attempt - b.AttemptID = 3 - _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, &b) + b = genAttemptWithHash( + t, 3, genSessionKey(t), rhash, + ) + + b.Route.FinalHop().AmtToForward = shardAmt + b.Route.FinalHop().MPP = record.NewMPP( + info.Value, [32]byte{1}, + ) + + _, err = paymentDB.RegisterAttempt( + ctx, info.PaymentIdentifier, b, + ) if test.settleFirst { require.ErrorIs( t, err, ErrPaymentPendingSettled, @@ -1963,7 +2298,7 @@ func TestMultiShard(t *testing.T) { if test.settleLast { // Settle the last outstanding attempt. _, err = paymentDB.SettleAttempt( - info.PaymentIdentifier, a.AttemptID, + ctx, info.PaymentIdentifier, a.AttemptID, &HTLCSettleInfo{ Preimage: preimg, }, @@ -1978,7 +2313,7 @@ func TestMultiShard(t *testing.T) { } else { // Fail the attempt. _, err := paymentDB.FailAttempt( - info.PaymentIdentifier, a.AttemptID, + ctx, info.PaymentIdentifier, a.AttemptID, &HTLCFailInfo{ Reason: htlcFail, }, @@ -2001,7 +2336,7 @@ func TestMultiShard(t *testing.T) { // syncing. failReason := FailureReasonPaymentDetails _, err = paymentDB.Fail( - info.PaymentIdentifier, failReason, + ctx, info.PaymentIdentifier, failReason, ) require.NoError(t, err, "unable to fail") } @@ -2039,8 +2374,10 @@ func TestMultiShard(t *testing.T) { ) // Finally assert we cannot register more attempts. - _, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, &b) - require.Equal(t, registerErr, err) + _, err = paymentDB.RegisterAttempt( + ctx, info.PaymentIdentifier, b, + ) + require.ErrorIs(t, err, registerErr) } for _, test := range tests { @@ -2052,3 +2389,945 @@ func TestMultiShard(t *testing.T) { }) } } + +// TestQueryPayments tests retrieval of payments with forwards and reversed +// queries. +func TestQueryPayments(t *testing.T) { + // Define table driven test for QueryPayments. + // Test payments have sequence indices [1, 3, 4, 5, 6]. + // Note that payment with index 2 is deleted to create a gap in the + // sequence numbers. + tests := []struct { + name string + query Query + firstIndex uint64 + lastIndex uint64 + + // expectedSeqNrs contains the set of sequence numbers we expect + // our query to return. + expectedSeqNrs []uint64 + }{ + { + name: "IndexOffset at the end of the payments range", + query: Query{ + IndexOffset: 6, + MaxPayments: 7, + Reversed: false, + IncludeIncomplete: true, + }, + firstIndex: 0, + lastIndex: 0, + expectedSeqNrs: nil, + }, + { + name: "query in forwards order, start at beginning", + query: Query{ + IndexOffset: 0, + MaxPayments: 2, + Reversed: false, + IncludeIncomplete: true, + }, + firstIndex: 1, + lastIndex: 3, + expectedSeqNrs: []uint64{1, 3}, + }, + { + name: "query in forwards order, start at end, overflow", + query: Query{ + IndexOffset: 5, + MaxPayments: 2, + Reversed: false, + IncludeIncomplete: true, + }, + firstIndex: 6, + lastIndex: 6, + expectedSeqNrs: []uint64{6}, + }, + { + name: "start at offset index outside of payments", + query: Query{ + IndexOffset: 20, + MaxPayments: 2, + Reversed: false, + IncludeIncomplete: true, + }, + firstIndex: 0, + lastIndex: 0, + expectedSeqNrs: nil, + }, + { + name: "overflow in forwards order", + query: Query{ + IndexOffset: 4, + MaxPayments: math.MaxUint64, + Reversed: false, + IncludeIncomplete: true, + }, + firstIndex: 5, + lastIndex: 6, + expectedSeqNrs: []uint64{5, 6}, + }, + { + name: "start at offset index outside of payments, " + + "reversed order", + query: Query{ + IndexOffset: 9, + MaxPayments: 2, + Reversed: true, + IncludeIncomplete: true, + }, + firstIndex: 5, + lastIndex: 6, + expectedSeqNrs: []uint64{5, 6}, + }, + { + name: "query in reverse order, start at end", + query: Query{ + IndexOffset: 0, + MaxPayments: 2, + Reversed: true, + IncludeIncomplete: true, + }, + firstIndex: 5, + lastIndex: 6, + expectedSeqNrs: []uint64{5, 6}, + }, + { + name: "query in reverse order, starting in middle", + query: Query{ + IndexOffset: 4, + MaxPayments: 2, + Reversed: true, + IncludeIncomplete: true, + }, + firstIndex: 1, + lastIndex: 3, + expectedSeqNrs: []uint64{1, 3}, + }, + { + name: "query in reverse order, starting in middle, " + + "with underflow", + query: Query{ + IndexOffset: 4, + MaxPayments: 5, + Reversed: true, + IncludeIncomplete: true, + }, + firstIndex: 1, + lastIndex: 3, + expectedSeqNrs: []uint64{1, 3}, + }, + { + name: "all payments in reverse, order maintained", + query: Query{ + IndexOffset: 0, + MaxPayments: 7, + Reversed: true, + IncludeIncomplete: true, + }, + firstIndex: 1, + lastIndex: 6, + expectedSeqNrs: []uint64{1, 3, 4, 5, 6}, + }, + { + name: "exclude incomplete payments", + query: Query{ + IndexOffset: 0, + MaxPayments: 7, + Reversed: false, + IncludeIncomplete: false, + }, + firstIndex: 6, + lastIndex: 6, + expectedSeqNrs: []uint64{6}, + }, + { + name: "query payments at index gap", + query: Query{ + IndexOffset: 1, + MaxPayments: 7, + Reversed: false, + IncludeIncomplete: true, + }, + firstIndex: 3, + lastIndex: 6, + expectedSeqNrs: []uint64{3, 4, 5, 6}, + }, + { + name: "query payments reverse before index gap", + query: Query{ + IndexOffset: 3, + MaxPayments: 7, + Reversed: true, + IncludeIncomplete: true, + }, + firstIndex: 1, + lastIndex: 1, + expectedSeqNrs: []uint64{1}, + }, + { + name: "query payments reverse on index gap", + query: Query{ + IndexOffset: 2, + MaxPayments: 7, + Reversed: true, + IncludeIncomplete: true, + }, + firstIndex: 1, + lastIndex: 1, + expectedSeqNrs: []uint64{1}, + }, + { + name: "query payments forward on index gap", + query: Query{ + IndexOffset: 2, + MaxPayments: 2, + Reversed: false, + IncludeIncomplete: true, + }, + firstIndex: 3, + lastIndex: 4, + expectedSeqNrs: []uint64{3, 4}, + }, + { + name: "query in forwards order, with start creation " + + "time", + query: Query{ + IndexOffset: 0, + MaxPayments: 2, + Reversed: false, + IncludeIncomplete: true, + CreationDateStart: 5, + }, + firstIndex: 5, + lastIndex: 6, + expectedSeqNrs: []uint64{5, 6}, + }, + { + name: "query in forwards order, with start creation " + + "time at end, overflow", + query: Query{ + IndexOffset: 0, + MaxPayments: 2, + Reversed: false, + IncludeIncomplete: true, + CreationDateStart: 6, + }, + firstIndex: 6, + lastIndex: 6, + expectedSeqNrs: []uint64{6}, + }, + { + name: "query with start and end creation time", + query: Query{ + IndexOffset: 9, + MaxPayments: math.MaxUint64, + Reversed: true, + IncludeIncomplete: true, + CreationDateStart: 3, + CreationDateEnd: 5, + }, + firstIndex: 3, + lastIndex: 5, + expectedSeqNrs: []uint64{3, 4, 5}, + }, + { + name: "query with only end creation time", + query: Query{ + IndexOffset: 0, + MaxPayments: math.MaxUint64, + Reversed: false, + IncludeIncomplete: true, + CreationDateEnd: 4, + }, + firstIndex: 1, + lastIndex: 4, + expectedSeqNrs: []uint64{1, 3, 4}, + }, + { + name: "query reversed with creation date start", + query: Query{ + IndexOffset: 0, + MaxPayments: 3, + Reversed: true, + IncludeIncomplete: true, + CreationDateStart: 3, + }, + firstIndex: 4, + lastIndex: 6, + expectedSeqNrs: []uint64{4, 5, 6}, + }, + { + name: "count total with forward pagination", + query: Query{ + IndexOffset: 0, + MaxPayments: 2, + Reversed: false, + IncludeIncomplete: true, + CountTotal: true, + }, + firstIndex: 1, + lastIndex: 3, + expectedSeqNrs: []uint64{1, 3}, + }, + { + name: "count total with reverse pagination", + query: Query{ + IndexOffset: 0, + MaxPayments: 2, + Reversed: true, + IncludeIncomplete: true, + CountTotal: true, + }, + firstIndex: 5, + lastIndex: 6, + expectedSeqNrs: []uint64{5, 6}, + }, + { + name: "count total with filters", + query: Query{ + IndexOffset: 0, + MaxPayments: math.MaxUint64, + Reversed: false, + IncludeIncomplete: false, + CountTotal: true, + }, + firstIndex: 6, + lastIndex: 6, + expectedSeqNrs: []uint64{6}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + ctx := t.Context() + + paymentDB, harness := NewTestDB(t) + + // Make a preliminary query to make sure it's ok to + // query when we have no payments. + resp, err := paymentDB.QueryPayments(ctx, tt.query) + require.NoError(t, err) + require.Len(t, resp.Payments, 0) + + // Populate the database with a set of test payments. + // We create 6 payments, deleting the payment at index + // 2 so that we cover the case where sequence numbers + // are missing. + numberOfPayments := 6 + + // Store payment info for all payments so we can delete + // one after all are created. + var paymentInfos []*PaymentCreationInfo + + // First, create all payments. + for i := range numberOfPayments { + // Generate a test payment. + info, _ := genInfo(t) + + // Override creation time to allow for testing + // of CreationDateStart and CreationDateEnd. + info.CreationTime = time.Unix(int64(i+1), 0) + + paymentInfos = append(paymentInfos, info) + + // Create a new payment entry in the database. + err = paymentDB.InitPayment( + ctx, info.PaymentIdentifier, info, + ) + require.NoError(t, err) + } + + // Now delete the payment at index 1 (the second + // payment). + pmt, err := paymentDB.FetchPayment( + ctx, paymentInfos[1].PaymentIdentifier, + ) + require.NoError(t, err) + + // We delete the whole payment. + err = paymentDB.DeletePayment( + ctx, paymentInfos[1].PaymentIdentifier, false, + ) + require.NoError(t, err) + + // Verify the payment is deleted. + _, err = paymentDB.FetchPayment( + ctx, paymentInfos[1].PaymentIdentifier, + ) + require.ErrorIs( + t, err, ErrPaymentNotInitiated, + ) + + // Verify the index is removed (KV store only). + harness.AssertNoIndex( + t, pmt.SequenceNum, + ) + + // For the last payment, settle it so we have at least + // one completed payment for the "exclude incomplete" + // test case. + lastPaymentInfo := paymentInfos[numberOfPayments-1] + attempt, err := NewHtlcAttempt( + 1, priv, testRoute, + time.Unix(100, 0), + &lastPaymentInfo.PaymentIdentifier, + ) + require.NoError(t, err) + + _, err = paymentDB.RegisterAttempt( + ctx, lastPaymentInfo.PaymentIdentifier, + &attempt.HTLCAttemptInfo, + ) + require.NoError(t, err) + + var preimg lntypes.Preimage + copy(preimg[:], rev[:]) + + _, err = paymentDB.SettleAttempt( + ctx, lastPaymentInfo.PaymentIdentifier, + attempt.AttemptID, + &HTLCSettleInfo{ + Preimage: preimg, + }, + ) + require.NoError(t, err) + + // Fetch all payments in the database. + resp, err = paymentDB.QueryPayments( + ctx, Query{ + IndexOffset: 0, + MaxPayments: math.MaxUint64, + IncludeIncomplete: true, + }, + ) + require.NoError(t, err) + + allPayments := resp.Payments + + if len(allPayments) != 5 { + t.Fatalf("Number of payments received does "+ + "not match expected one. Got %v, "+ + "want %v.", len(allPayments), 5) + } + + querySlice, err := paymentDB.QueryPayments( + ctx, tt.query, + ) + require.NoError(t, err) + + if tt.firstIndex != querySlice.FirstIndexOffset || + tt.lastIndex != querySlice.LastIndexOffset { + + t.Errorf("First or last index does not match "+ + "expected index. Want (%d, %d), "+ + "got (%d, %d).", + tt.firstIndex, tt.lastIndex, + querySlice.FirstIndexOffset, + querySlice.LastIndexOffset) + } + + if len(querySlice.Payments) != len(tt.expectedSeqNrs) { + t.Errorf("expected: %v payments, got: %v", + len(tt.expectedSeqNrs), + len(querySlice.Payments)) + } + + for i, seqNr := range tt.expectedSeqNrs { + q := querySlice.Payments[i] + if seqNr != q.SequenceNum { + t.Errorf("sequence numbers do not "+ + "match, got %v, want %v", + q.SequenceNum, seqNr) + } + } + + // Verify CountTotal is set correctly when requested. + if tt.query.CountTotal { + // We should have 5 total payments + // (6 created - 1 deleted). + expectedTotal := uint64(5) + require.Equal( + t, expectedTotal, querySlice.TotalCount, + "expected total count %v, got %v", + expectedTotal, querySlice.TotalCount) + } else { + require.Equal( + t, uint64(0), querySlice.TotalCount, + "expected total count 0 when "+ + "CountTotal=false") + } + }) + } +} + +// TestFetchInFlightPayments tests that FetchInFlightPayments correctly returns +// only payments that are in-flight. +func TestFetchInFlightPayments(t *testing.T) { + t.Parallel() + + ctx := t.Context() + + paymentDB, _ := NewTestDB(t) + + // Register payments with different statuses: + // 1. A payment with two failed attempts (StatusFailed). + // 2. A payment with one failed and one settled attempt + // (StatusSucceeded). + // 3. A payment with one failed and one in-flight attempt + // (StatusInFlight). + // 4. Another payment with one failed and one in-flight attempt + // (StatusInFlight). + payments := []*payment{ + {status: StatusFailed}, + {status: StatusSucceeded}, + {status: StatusInFlight}, + {status: StatusInFlight}, + } + + // Use helper function to register the test payments in the database and + // populate the data to the payments slice. + createTestPayments(t, paymentDB, payments) + + // Check that all payments are there as we added them. + assertDBPayments(t, paymentDB, payments) + + // Fetch in-flight payments. + inFlightPayments, err := paymentDB.FetchInFlightPayments(ctx) + require.NoError(t, err) + + // We should only get the two in-flight payments. + require.Len(t, inFlightPayments, 2) + + // Verify that the returned payments are the in-flight ones. + inFlightHashes := make(map[lntypes.Hash]struct{}) + for _, p := range inFlightPayments { + require.Equal(t, StatusInFlight, p.Status) + inFlightHashes[p.Info.PaymentIdentifier] = struct{}{} + } + + // Check that the in-flight payments match the expected ones. + require.Contains(t, inFlightHashes, payments[2].id) + require.Contains(t, inFlightHashes, payments[3].id) + + // Now settle one of the in-flight payments. + preimg := genPreimage(t) + + _, err = paymentDB.SettleAttempt( + ctx, payments[2].id, 5, + &HTLCSettleInfo{ + Preimage: preimg, + }, + ) + require.NoError(t, err) + + // Fetch in-flight payments again. + inFlightPayments, err = paymentDB.FetchInFlightPayments(ctx) + require.NoError(t, err) + + // We should now only get one in-flight payment. + require.Len(t, inFlightPayments, 1) + require.Equal( + t, payments[3].id, + inFlightPayments[0].Info.PaymentIdentifier, + ) + require.Equal(t, StatusInFlight, inFlightPayments[0].Status) +} + +// TestFetchInFlightPaymentsMultipleAttempts tests that when fetching in-flight +// payments, a payment with multiple in-flight attempts is only returned once. +func TestFetchInFlightPaymentsMultipleAttempts(t *testing.T) { + t.Parallel() + + ctx := t.Context() + + paymentDB, _ := NewTestDB(t) + + preimg := genPreimage(t) + + rhash := sha256.Sum256(preimg[:]) + info := genPaymentCreationInfo(t, rhash) + + // Init payment with double the amount to allow two attempts. + info.Value *= 2 + err := paymentDB.InitPayment(ctx, info.PaymentIdentifier, info) + require.NoError(t, err) + + // Register two attempts for the same payment. + attempt1 := genAttemptWithHash(t, 0, genSessionKey(t), rhash) + + _, err = paymentDB.RegisterAttempt( + ctx, info.PaymentIdentifier, attempt1, + ) + require.NoError(t, err) + + attempt2 := genAttemptWithHash(t, 1, genSessionKey(t), rhash) + + _, err = paymentDB.RegisterAttempt( + ctx, info.PaymentIdentifier, attempt2, + ) + require.NoError(t, err) + + // Both attempts are in-flight. Fetch in-flight payments. + inFlightPayments, err := paymentDB.FetchInFlightPayments(ctx) + require.NoError(t, err) + + // We should only get one payment even though it has 2 in-flight + // attempts. + require.Len(t, inFlightPayments, 1) + require.Equal( + t, info.PaymentIdentifier, + inFlightPayments[0].Info.PaymentIdentifier, + ) + require.Equal(t, StatusInFlight, inFlightPayments[0].Status) + + // Verify the payment has both attempts. + require.Len(t, inFlightPayments[0].HTLCs, 2) +} + +// TestRouteFirstHopData tests that Route.FirstHopAmount and +// Route.FirstHopWireCustomRecords are correctly stored and retrieved. +func TestRouteFirstHopData(t *testing.T) { + t.Parallel() + + ctx := t.Context() + + paymentDB, _ := NewTestDB(t) + + preimg := genPreimage(t) + rhash := sha256.Sum256(preimg[:]) + info := genPaymentCreationInfo(t, rhash) + firstHopAmount := lnwire.MilliSatoshi(1234) + + // Init payment. + err := paymentDB.InitPayment(ctx, info.PaymentIdentifier, info) + require.NoError(t, err) + + // Create an attempt with both FirstHopAmount and + // FirstHopWireCustomRecords set on the route. + attempt := genAttemptWithHash(t, 0, genSessionKey(t), rhash) + attempt.Route.FirstHopAmount = tlv.NewRecordT[tlv.TlvType0]( + tlv.NewBigSizeT(firstHopAmount), + ) + typeIdx1 := uint64(lnwire.MinCustomRecordsTlvType + 10) + typeIdx2 := uint64(lnwire.MinCustomRecordsTlvType + 20) + attempt.Route.FirstHopWireCustomRecords = lnwire.CustomRecords{ + typeIdx1: []byte("wire_record_1"), + typeIdx2: []byte("wire_record_2"), + } + + _, err = paymentDB.RegisterAttempt(ctx, info.PaymentIdentifier, attempt) + require.NoError(t, err) + + // Fetch the payment and verify first hop data was stored. + payment, err := paymentDB.FetchPayment(ctx, info.PaymentIdentifier) + require.NoError(t, err) + + require.Len(t, payment.HTLCs, 1) + htlc := payment.HTLCs[0] + + // Verify the FirstHopAmount matches what we set. + require.NotNil(t, htlc.Route.FirstHopAmount) + require.Equal( + t, firstHopAmount, + htlc.Route.FirstHopAmount.Val.Int(), + ) + + // Verify the FirstHopWireCustomRecords match what we set. + require.NotEmpty(t, htlc.Route.FirstHopWireCustomRecords) + require.Len(t, htlc.Route.FirstHopWireCustomRecords, 2) + require.Equal( + t, []byte("wire_record_1"), + htlc.Route.FirstHopWireCustomRecords[typeIdx1], + ) + require.Equal( + t, []byte("wire_record_2"), + htlc.Route.FirstHopWireCustomRecords[typeIdx2], + ) +} + +// TestRegisterAttemptWithAMP tests that AMP data is correctly stored and +// retrieved on route hops. +func TestRegisterAttemptWithAMP(t *testing.T) { + t.Parallel() + + ctx := t.Context() + + paymentDB, _ := NewTestDB(t) + + preimg := genPreimage(t) + rhash := sha256.Sum256(preimg[:]) + info := genPaymentCreationInfo(t, rhash) + + // Init payment. + err := paymentDB.InitPayment(ctx, info.PaymentIdentifier, info) + require.NoError(t, err) + + // Create a basic attempt, then modify the route to include AMP data. + // This bypasses the route validation in NewHtlcAttempt. + attempt := genAttemptWithHash(t, 0, genSessionKey(t), rhash) + + // Add AMP data to the final hop. + rootShare := [32]byte{1, 2, 3, 4} + setID := [32]byte{5, 6, 7, 8} + childIndex := uint32(42) + + finalHopIdx := len(attempt.Route.Hops) - 1 + attempt.Route.Hops[finalHopIdx].AMP = record.NewAMP( + rootShare, setID, childIndex, + ) + + _, err = paymentDB.RegisterAttempt(ctx, info.PaymentIdentifier, attempt) + require.NoError(t, err) + + // Fetch the payment and verify AMP data was stored. + payment, err := paymentDB.FetchPayment(ctx, info.PaymentIdentifier) + require.NoError(t, err) + + require.Len(t, payment.HTLCs, 1) + htlc := payment.HTLCs[0] + + // Verify the AMP data on the final hop matches what we set. + finalHop := htlc.Route.Hops[finalHopIdx] + require.NotNil(t, finalHop.AMP) + require.Equal(t, rootShare, finalHop.AMP.RootShare()) + require.Equal(t, setID, finalHop.AMP.SetID()) + require.Equal(t, childIndex, finalHop.AMP.ChildIndex()) +} + +// TestRegisterAttemptWithBlindedRoute tests that blinded route data +// (EncryptedData, BlindingPoint, TotalAmtMsat) is correctly stored and +// retrieved. +func TestRegisterAttemptWithBlindedRoute(t *testing.T) { + t.Parallel() + + ctx := t.Context() + + paymentDB, _ := NewTestDB(t) + + preimg := genPreimage(t) + rhash := sha256.Sum256(preimg[:]) + + // Create payment info with amount matching + // testBlindedRoute.TotalAmount. + info := &PaymentCreationInfo{ + PaymentIdentifier: rhash, + Value: testBlindedRoute.TotalAmount, + CreationTime: time.Unix(time.Now().Unix(), 0), + PaymentRequest: []byte("blinded"), + } + + // Init payment. + err := paymentDB.InitPayment(ctx, info.PaymentIdentifier, info) + require.NoError(t, err) + + // Create a basic attempt, then replace the route with testBlindedRoute. + // This bypasses the route validation in NewHtlcAttempt. + attempt := genAttemptWithHash(t, 0, genSessionKey(t), rhash) + + // Replace with testBlindedRoute which has the correct blinded route + // structure. + attempt.Route = testBlindedRoute + + _, err = paymentDB.RegisterAttempt(ctx, info.PaymentIdentifier, attempt) + require.NoError(t, err) + + // Fetch the payment and verify blinded route data was stored. + payment, err := paymentDB.FetchPayment(ctx, info.PaymentIdentifier) + require.NoError(t, err) + + require.Len(t, payment.HTLCs, 1) + htlc := payment.HTLCs[0] + + // Verify the blinded route data. + require.Len(t, htlc.Route.Hops, 3) + + // First hop (introduction point) should have BlindingPoint and + // EncryptedData. + hop0 := htlc.Route.Hops[0] + require.Equal(t, []byte{1, 3, 3}, hop0.EncryptedData) + require.NotNil(t, hop0.BlindingPoint) + require.True(t, hop0.BlindingPoint.IsEqual(pub)) + + // Second hop (intermediate) should have only EncryptedData. + hop1 := htlc.Route.Hops[1] + require.Equal(t, []byte{3, 2, 1}, hop1.EncryptedData) + require.Nil(t, hop1.BlindingPoint) + + // Third hop (final) should have EncryptedData, AmtToForward, + // OutgoingTimeLock, and TotalAmtMsat. + hop2 := htlc.Route.Hops[2] + require.Equal(t, []byte{2, 2, 2}, hop2.EncryptedData) + require.Equal(t, lnwire.MilliSatoshi(1000), hop2.AmtToForward) + require.Equal(t, uint32(100), hop2.OutgoingTimeLock) + require.Equal(t, lnwire.MilliSatoshi(1000), hop2.TotalAmtMsat) +} + +// TestFailAttemptWithoutMessage tests that FailAttempt works correctly when +// no failure message is provided. +func TestFailAttemptWithoutMessage(t *testing.T) { + t.Parallel() + + ctx := t.Context() + + paymentDB, _ := NewTestDB(t) + + preimg := genPreimage(t) + rhash := sha256.Sum256(preimg[:]) + info := genPaymentCreationInfo(t, rhash) + + // Init payment. + err := paymentDB.InitPayment(ctx, info.PaymentIdentifier, info) + require.NoError(t, err) + + // Register an attempt. + attempt := genAttemptWithHash(t, 0, genSessionKey(t), rhash) + + _, err = paymentDB.RegisterAttempt(ctx, info.PaymentIdentifier, attempt) + require.NoError(t, err) + + // Fail the attempt without a failure message (nil Message). + failInfo := &HTLCFailInfo{ + Reason: HTLCFailUnreadable, + FailureSourceIndex: 2, + Message: nil, // No message. + } + + payment, err := paymentDB.FailAttempt( + ctx, info.PaymentIdentifier, attempt.AttemptID, failInfo, + ) + require.NoError(t, err) + require.NotNil(t, payment) + + // Verify the attempt was failed. + require.Len(t, payment.HTLCs, 1) + htlc := payment.HTLCs[0] + require.NotNil(t, htlc.Failure) + require.Equal(t, HTLCFailUnreadable, htlc.Failure.Reason) + require.Equal(t, uint32(2), htlc.Failure.FailureSourceIndex) + require.Nil(t, htlc.Failure.Message) +} + +// TestFailAttemptWithMessage tests that FailAttempt correctly stores and +// retrieves a failure message. +func TestFailAttemptWithMessage(t *testing.T) { + t.Parallel() + + ctx := t.Context() + + paymentDB, _ := NewTestDB(t) + + preimg := genPreimage(t) + rhash := sha256.Sum256(preimg[:]) + info := genPaymentCreationInfo(t, rhash) + + // Init payment. + err := paymentDB.InitPayment(ctx, info.PaymentIdentifier, info) + require.NoError(t, err) + + // Register an attempt. + attempt := genAttemptWithHash(t, 0, genSessionKey(t), rhash) + + _, err = paymentDB.RegisterAttempt(ctx, info.PaymentIdentifier, attempt) + require.NoError(t, err) + + // Create a failure message. + failureMsg := lnwire.NewTemporaryChannelFailure(nil) + + // Fail the attempt with a failure message. + failInfo := &HTLCFailInfo{ + Reason: HTLCFailUnreadable, + FailureSourceIndex: 1, + Message: failureMsg, + } + + payment, err := paymentDB.FailAttempt( + ctx, info.PaymentIdentifier, attempt.AttemptID, failInfo, + ) + require.NoError(t, err) + require.NotNil(t, payment) + + // Verify the attempt was failed. + require.Len(t, payment.HTLCs, 1) + htlc := payment.HTLCs[0] + require.NotNil(t, htlc.Failure) + require.Equal(t, HTLCFailUnreadable, htlc.Failure.Reason) +} + +// TestFailAttemptOnSucceededPayment tests that FailAttempt returns an error +// when trying to fail an attempt on an already succeeded payment. +func TestFailAttemptOnSucceededPayment(t *testing.T) { + t.Parallel() + + ctx := t.Context() + + paymentDB, _ := NewTestDB(t) + + preimg := genPreimage(t) + rhash := sha256.Sum256(preimg[:]) + info := genPaymentCreationInfo(t, rhash) + + // Init payment. + err := paymentDB.InitPayment(ctx, info.PaymentIdentifier, info) + require.NoError(t, err) + + // Register an attempt. + attempt := genAttemptWithHash(t, 0, genSessionKey(t), rhash) + + _, err = paymentDB.RegisterAttempt(ctx, info.PaymentIdentifier, attempt) + require.NoError(t, err) + + // Settle the attempt, which makes the payment succeed. + _, err = paymentDB.SettleAttempt( + ctx, info.PaymentIdentifier, attempt.AttemptID, + &HTLCSettleInfo{Preimage: preimg}, + ) + require.NoError(t, err) + + // Now try to fail the same attempt - this should fail because the + // payment is already succeeded. + failInfo := &HTLCFailInfo{ + Reason: HTLCFailUnreadable, + } + + _, err = paymentDB.FailAttempt( + ctx, info.PaymentIdentifier, attempt.AttemptID, failInfo, + ) + require.Error(t, err) + require.ErrorIs(t, err, ErrPaymentAlreadySucceeded) +} + +// TestFetchPaymentWithNoAttempts tests that FetchPayment correctly returns a +// payment that has been initialized but has no HTLC attempts yet. This tests +// the early return path in batchLoadPaymentDetailsData when there are no +// attempts. +func TestFetchPaymentWithNoAttempts(t *testing.T) { + t.Parallel() + + ctx := t.Context() + + paymentDB, _ := NewTestDB(t) + + preimg := genPreimage(t) + rhash := sha256.Sum256(preimg[:]) + info := genPaymentCreationInfo(t, rhash) + + // Init payment but don't register any attempts. + err := paymentDB.InitPayment(ctx, info.PaymentIdentifier, info) + require.NoError(t, err) + + // Fetch the payment - it should have no HTLCs. + payment, err := paymentDB.FetchPayment(ctx, info.PaymentIdentifier) + require.NoError(t, err) + require.NotNil(t, payment) + + // Verify the payment has no HTLCs. + require.Empty(t, payment.HTLCs) + + // Verify the payment info is correct. + require.Equal(t, info.PaymentIdentifier, payment.Info.PaymentIdentifier) + require.Equal(t, info.Value, payment.Info.Value) + require.Equal(t, StatusInitiated, payment.Status) +} diff --git a/payments/db/sql_converters.go b/payments/db/sql_converters.go new file mode 100644 index 00000000000..f6274bf5f05 --- /dev/null +++ b/payments/db/sql_converters.go @@ -0,0 +1,332 @@ +package paymentsdb + +import ( + "bytes" + "fmt" + "strconv" + "time" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/record" + "github.com/lightningnetwork/lnd/routing/route" + "github.com/lightningnetwork/lnd/sqldb/sqlc" + "github.com/lightningnetwork/lnd/tlv" +) + +// dbPaymentToCreationInfo converts database payment data to the +// PaymentCreationInfo struct. +func dbPaymentToCreationInfo(paymentIdentifier []byte, amountMsat int64, + createdAt time.Time, intentPayload []byte, + firstHopCustomRecords lnwire.CustomRecords) *PaymentCreationInfo { + + // This is the payment hash for non-AMP payments and the SetID for AMP + // payments. + var identifier lntypes.Hash + copy(identifier[:], paymentIdentifier) + + return &PaymentCreationInfo{ + PaymentIdentifier: identifier, + Value: lnwire.MilliSatoshi(amountMsat), + // The creation time is stored in the database as UTC but here + // we convert it to local time. + CreationTime: createdAt.Local(), + PaymentRequest: intentPayload, + FirstHopCustomRecords: firstHopCustomRecords, + } +} + +// dbAttemptToHTLCAttempt converts a database HTLC attempt to an HTLCAttempt. +func dbAttemptToHTLCAttempt(dbAttempt sqlc.FetchHtlcAttemptsForPaymentsRow, + hops []sqlc.FetchHopsForAttemptsRow, + hopCustomRecords map[int64][]sqlc.PaymentHopCustomRecord, + routeCustomRecords []sqlc.PaymentAttemptFirstHopCustomRecord) ( + *HTLCAttempt, error) { + + // Convert route-level first hop custom records to CustomRecords map. + var firstHopWireCustomRecords lnwire.CustomRecords + if len(routeCustomRecords) > 0 { + firstHopWireCustomRecords = make(lnwire.CustomRecords) + for _, record := range routeCustomRecords { + firstHopWireCustomRecords[uint64(record.Key)] = + record.Value + } + } + + // Build the route from the database data. + route, err := dbDataToRoute( + hops, hopCustomRecords, dbAttempt.FirstHopAmountMsat, + dbAttempt.RouteTotalTimeLock, dbAttempt.RouteTotalAmount, + dbAttempt.RouteSourceKey, firstHopWireCustomRecords, + ) + if err != nil { + return nil, fmt.Errorf("failed to convert to route: %w", + err) + } + + hash, err := lntypes.MakeHash(dbAttempt.PaymentHash) + if err != nil { + return nil, fmt.Errorf("failed to parse payment "+ + "hash: %w", err) + } + + // Create the attempt info. + var sessionKey [32]byte + copy(sessionKey[:], dbAttempt.SessionKey) + + info := HTLCAttemptInfo{ + AttemptID: uint64(dbAttempt.AttemptIndex), + sessionKey: sessionKey, + Route: *route, + AttemptTime: dbAttempt.AttemptTime, + Hash: &hash, + } + + attempt := &HTLCAttempt{ + HTLCAttemptInfo: info, + } + + // If there's no resolution type, the attempt is still in-flight. + // Return early without processing settlement or failure info. + if !dbAttempt.ResolutionType.Valid { + return attempt, nil + } + + // Add settlement info if present. + if HTLCAttemptResolutionType(dbAttempt.ResolutionType.Int32) == + HTLCAttemptResolutionSettled { + + var preimage lntypes.Preimage + copy(preimage[:], dbAttempt.SettlePreimage) + + attempt.Settle = &HTLCSettleInfo{ + Preimage: preimage, + SettleTime: dbAttempt.ResolutionTime.Time, + } + } + + // Add failure info if present. + if HTLCAttemptResolutionType(dbAttempt.ResolutionType.Int32) == + HTLCAttemptResolutionFailed { + + failure := &HTLCFailInfo{ + FailTime: dbAttempt.ResolutionTime.Time, + } + + if dbAttempt.HtlcFailReason.Valid { + failure.Reason = HTLCFailReason( + dbAttempt.HtlcFailReason.Int32, + ) + } + + if dbAttempt.FailureSourceIndex.Valid { + failure.FailureSourceIndex = uint32( + dbAttempt.FailureSourceIndex.Int32, + ) + } + + // Decode the failure message if present. + if len(dbAttempt.FailureMsg) > 0 { + msg, err := lnwire.DecodeFailureMessage( + bytes.NewReader(dbAttempt.FailureMsg), 0, + ) + if err != nil { + return nil, fmt.Errorf("failed to decode "+ + "failure message: %w", err) + } + failure.Message = msg + } + + attempt.Failure = failure + } + + return attempt, nil +} + +// dbDataToRoute converts database route data to a route.Route. +func dbDataToRoute(hops []sqlc.FetchHopsForAttemptsRow, + hopCustomRecords map[int64][]sqlc.PaymentHopCustomRecord, + firstHopAmountMsat int64, totalTimeLock int32, totalAmount int64, + sourceKey []byte, firstHopWireCustomRecords lnwire.CustomRecords) ( + *route.Route, error) { + + if len(hops) == 0 { + return nil, fmt.Errorf("no hops provided") + } + + // Hops are already sorted by hop_index from the SQL query. + routeHops := make([]*route.Hop, len(hops)) + + for i, hop := range hops { + pubKey, err := route.NewVertexFromBytes(hop.PubKey) + if err != nil { + return nil, fmt.Errorf("failed to parse pub key: %w", + err) + } + + var channelID uint64 + if hop.Scid != "" { + // The SCID is stored as a string representation + // of the uint64. + var err error + channelID, err = strconv.ParseUint(hop.Scid, 10, 64) + if err != nil { + return nil, fmt.Errorf("failed to parse "+ + "scid: %w", err) + } + } + + routeHop := &route.Hop{ + PubKeyBytes: pubKey, + ChannelID: channelID, + OutgoingTimeLock: uint32(hop.OutgoingTimeLock), + AmtToForward: lnwire.MilliSatoshi(hop.AmtToForward), + } + + // Add MPP record if present. + if len(hop.MppPaymentAddr) > 0 { + var paymentAddr [32]byte + copy(paymentAddr[:], hop.MppPaymentAddr) + routeHop.MPP = record.NewMPP( + lnwire.MilliSatoshi(hop.MppTotalMsat.Int64), + paymentAddr, + ) + } + + // Add AMP record if present. + if len(hop.AmpRootShare) > 0 { + var rootShare [32]byte + copy(rootShare[:], hop.AmpRootShare) + var setID [32]byte + copy(setID[:], hop.AmpSetID) + + routeHop.AMP = record.NewAMP( + rootShare, setID, + uint32(hop.AmpChildIndex.Int32), + ) + } + + // Add blinding point if present (only for introduction node + // in blinded route). + if len(hop.BlindingPoint) > 0 { + pubKey, err := btcec.ParsePubKey(hop.BlindingPoint) + if err != nil { + return nil, fmt.Errorf("failed to parse "+ + "blinding point: %w", err) + } + routeHop.BlindingPoint = pubKey + } + + // Add encrypted data if present (for all blinded hops). + if len(hop.EncryptedData) > 0 { + routeHop.EncryptedData = hop.EncryptedData + } + + // Add total amount if present (only for final hop in blinded + // route). + if hop.BlindedPathTotalAmt.Valid { + routeHop.TotalAmtMsat = lnwire.MilliSatoshi( + hop.BlindedPathTotalAmt.Int64, + ) + } + + // Add hop-level custom records. + if records, ok := hopCustomRecords[hop.ID]; ok { + routeHop.CustomRecords = make( + record.CustomSet, + ) + for _, rec := range records { + routeHop.CustomRecords[uint64(rec.Key)] = + rec.Value + } + } + + // Add metadata if present. + if len(hop.MetaData) > 0 { + routeHop.Metadata = hop.MetaData + } + + routeHops[i] = routeHop + } + + // Parse the source node public key. + var sourceNode route.Vertex + copy(sourceNode[:], sourceKey) + + route := &route.Route{ + TotalTimeLock: uint32(totalTimeLock), + TotalAmount: lnwire.MilliSatoshi(totalAmount), + SourcePubKey: sourceNode, + Hops: routeHops, + FirstHopWireCustomRecords: firstHopWireCustomRecords, + } + + // Set the first hop amount if it is set. + if firstHopAmountMsat != 0 { + route.FirstHopAmount = tlv.NewRecordT[tlv.TlvType0]( + tlv.NewBigSizeT(lnwire.MilliSatoshi( + firstHopAmountMsat, + )), + ) + } + + return route, nil +} + +// dbDuplicatePaymentsToDuplicatePayments converts SQL duplicate rows into +// domain records. +func dbDuplicatePaymentsToDuplicatePayments(rows []sqlc.PaymentDuplicate) ( + []*DuplicatePayment, error) { + + if len(rows) == 0 { + return []*DuplicatePayment{}, nil + } + + duplicates := make([]*DuplicatePayment, 0, len(rows)) + for _, row := range rows { + var paymentHash lntypes.Hash + if len(row.PaymentIdentifier) != len(paymentHash) { + return nil, fmt.Errorf("invalid payment identifier "+ + "length: %d", len(row.PaymentIdentifier)) + } + copy(paymentHash[:], row.PaymentIdentifier) + + var failureReason *FailureReason + if row.FailReason.Valid { + reason := FailureReason(row.FailReason.Int32) + failureReason = &reason + } + + var settleInfo *HTLCSettleInfo + if len(row.SettlePreimage) > 0 || row.SettleTime.Valid { + var preimage lntypes.Preimage + switch len(row.SettlePreimage) { + case 0: + case lntypes.PreimageSize: + copy(preimage[:], row.SettlePreimage) + default: + return nil, fmt.Errorf("invalid settle "+ + "preimage length: %d", + len(row.SettlePreimage)) + } + + settleInfo = &HTLCSettleInfo{ + Preimage: preimage, + } + if row.SettleTime.Valid { + settleInfo.SettleTime = row.SettleTime.Time + } + } + + duplicates = append(duplicates, &DuplicatePayment{ + PaymentIdentifier: paymentHash, + Amount: lnwire.MilliSatoshi(row.AmountMsat), + CreationTime: row.CreatedAt, + FailureReason: failureReason, + Settle: settleInfo, + }) + } + + return duplicates, nil +} diff --git a/payments/db/sql_duplicate_payments_test.go b/payments/db/sql_duplicate_payments_test.go new file mode 100644 index 00000000000..f7c17ab6031 --- /dev/null +++ b/payments/db/sql_duplicate_payments_test.go @@ -0,0 +1,222 @@ +//go:build test_db_postgres || test_db_sqlite + +package paymentsdb + +import ( + "context" + "database/sql" + "testing" + "time" + + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/sqldb" + "github.com/lightningnetwork/lnd/sqldb/sqlc" + "github.com/stretchr/testify/require" +) + +// TestFetchDuplicatePayments verifies that duplicate payment records are +// returned with the expected failure or settlement data for a single payment. +func TestFetchDuplicatePayments(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + paymentDB, _ := NewTestDB(t) + sqlStore, ok := paymentDB.(*SQLStore) + require.True(t, ok) + + paymentHash := testHash + createdAt := time.Unix(100, 0).UTC() + amount := lnwire.MilliSatoshi(1000) + + dupFailedHash := makeTestHash(0x02) + dupSettledHash := makeTestHash(0x03) + + settlePreimage := makeTestPreimage(0x10) + settleTime := time.Unix(200, 0).UTC() + + duplicates := []sqlc.InsertPaymentDuplicateMigParams{ + { + PaymentIdentifier: dupFailedHash[:], + AmountMsat: int64(amount), + CreatedAt: createdAt.Add(time.Second), + FailReason: sql.NullInt32{ + Int32: int32(FailureReasonError), + Valid: true, + }, + }, + { + PaymentIdentifier: dupSettledHash[:], + AmountMsat: int64(amount + 100), + CreatedAt: createdAt.Add(2 * time.Second), + SettlePreimage: settlePreimage[:], + SettleTime: sql.NullTime{ + Time: settleTime, + Valid: true, + }, + }, + } + + insertTestPaymentWithDuplicates( + t, ctx, sqlStore, paymentHash, createdAt, amount, duplicates, + ) + + results, err := sqlStore.FetchDuplicatePayments(ctx, paymentHash) + require.NoError(t, err) + require.Len(t, results, 2) + + byHash := make(map[lntypes.Hash]*DuplicatePayment, len(results)) + for _, dup := range results { + byHash[dup.PaymentIdentifier] = dup + } + + failed := byHash[dupFailedHash] + require.NotNil(t, failed) + require.NotNil(t, failed.FailureReason) + require.Equal(t, FailureReasonError, *failed.FailureReason) + require.Nil(t, failed.Settle) + + settled := byHash[dupSettledHash] + require.NotNil(t, settled) + require.Nil(t, settled.FailureReason) + require.NotNil(t, settled.Settle) + require.Equal(t, settlePreimage, settled.Settle.Preimage) + require.True(t, settled.Settle.SettleTime.Equal(settleTime)) +} + +// TestFetchAllDuplicatePayments verifies cursor-based pagination over all +// duplicate payment records across multiple payments. +func TestFetchAllDuplicatePayments(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + queryCfg := sqldb.DefaultSQLiteConfig() + queryCfg.MaxPageSize = 1 + + sqlStore, err := NewSQLStore( + &SQLStoreConfig{ + QueryCfg: queryCfg, + }, + newBatchQuerier(t), + ) + require.NoError(t, err) + + createdAt := time.Unix(300, 0).UTC() + amount := lnwire.MilliSatoshi(2000) + + paymentHashA := makeTestHash(0x11) + dupA1 := makeTestHash(0x12) + dupA2 := makeTestHash(0x13) + + paymentHashB := makeTestHash(0x21) + dupB1 := makeTestHash(0x22) + + insertTestPaymentWithDuplicates( + t, ctx, sqlStore, paymentHashA, createdAt, amount, + []sqlc.InsertPaymentDuplicateMigParams{ + { + PaymentIdentifier: dupA1[:], + AmountMsat: int64(amount), + CreatedAt: createdAt.Add(time.Second), + FailReason: sql.NullInt32{ + Int32: int32(FailureReasonError), + Valid: true, + }, + }, + { + PaymentIdentifier: dupA2[:], + AmountMsat: int64(amount + 10), + CreatedAt: createdAt.Add( + 2 * time.Second, + ), + FailReason: sql.NullInt32{ + Int32: int32(FailureReasonError), + Valid: true, + }, + }, + }, + ) + + insertTestPaymentWithDuplicates( + t, ctx, sqlStore, paymentHashB, createdAt, amount, + []sqlc.InsertPaymentDuplicateMigParams{ + { + PaymentIdentifier: dupB1[:], + AmountMsat: int64(amount + 20), + CreatedAt: createdAt.Add( + 3 * time.Second, + ), + FailReason: sql.NullInt32{ + Int32: int32(FailureReasonError), + Valid: true, + }, + }, + }, + ) + + results, err := sqlStore.FetchAllDuplicatePayments(ctx) + require.NoError(t, err) + require.Len(t, results, 3) + + require.Equal(t, dupA1, results[0].PaymentIdentifier) + require.Equal(t, dupA2, results[1].PaymentIdentifier) + require.Equal(t, dupB1, results[2].PaymentIdentifier) +} + +// insertTestPaymentWithDuplicates inserts a test payment with duplicates +// into the database. + +func insertTestPaymentWithDuplicates(t *testing.T, ctx context.Context, + sqlStore *SQLStore, paymentHash lntypes.Hash, createdAt time.Time, + amount lnwire.MilliSatoshi, + duplicates []sqlc.InsertPaymentDuplicateMigParams) { + + t.Helper() + + err := sqlStore.db.ExecTx( + ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error { + paymentID, err := db.InsertPaymentMig( + ctx, sqlc.InsertPaymentMigParams{ + AmountMsat: int64(amount), + CreatedAt: createdAt, + PaymentIdentifier: paymentHash[:], + }, + ) + if err != nil { + return err + } + + for i := range duplicates { + dup := duplicates[i] + dup.PaymentID = paymentID + _, err := db.InsertPaymentDuplicateMig(ctx, dup) + if err != nil { + return err + } + } + + return nil + }, sqldb.NoOpReset, + ) + require.NoError(t, err) +} + +// makeTestHash creates a test hash with the given byte. +func makeTestHash(b byte) lntypes.Hash { + var hash lntypes.Hash + for i := 0; i < len(hash); i++ { + hash[i] = b + } + return hash +} + +// makeTestPreimage creates a test preimage with the given byte. +func makeTestPreimage(b byte) lntypes.Preimage { + var preimage lntypes.Preimage + for i := 0; i < len(preimage); i++ { + preimage[i] = b + } + return preimage +} diff --git a/payments/db/sql_store.go b/payments/db/sql_store.go new file mode 100644 index 00000000000..392aacdab3e --- /dev/null +++ b/payments/db/sql_store.go @@ -0,0 +1,2061 @@ +package paymentsdb + +import ( + "bytes" + "context" + "database/sql" + "errors" + "fmt" + "math" + "strconv" + "time" + + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" + "github.com/lightningnetwork/lnd/sqldb" + "github.com/lightningnetwork/lnd/sqldb/sqlc" +) + +// PaymentIntentType represents the type of payment intent. +type PaymentIntentType int16 + +const ( + // PaymentIntentTypeBolt11 indicates a BOLT11 invoice payment. + PaymentIntentTypeBolt11 PaymentIntentType = 0 +) + +// HTLCAttemptResolutionType represents the type of HTLC attempt resolution. +type HTLCAttemptResolutionType int32 + +const ( + // HTLCAttemptResolutionSettled indicates the HTLC attempt was settled + // successfully with a preimage. + HTLCAttemptResolutionSettled HTLCAttemptResolutionType = 1 + + // HTLCAttemptResolutionFailed indicates the HTLC attempt failed. + HTLCAttemptResolutionFailed HTLCAttemptResolutionType = 2 +) + +// SQLQueries is a subset of the sqlc.Querier interface that can be used to +// execute queries against the SQL payments tables. +// +//nolint:ll,interfacebloat +type SQLQueries interface { + /* + Payment DB read operations. + */ + FilterPayments(ctx context.Context, query sqlc.FilterPaymentsParams) ([]sqlc.FilterPaymentsRow, error) + FetchPayment(ctx context.Context, paymentIdentifier []byte) (sqlc.FetchPaymentRow, error) + FetchPaymentsByIDs(ctx context.Context, paymentIDs []int64) ([]sqlc.FetchPaymentsByIDsRow, error) + + CountPayments(ctx context.Context) (int64, error) + + FetchHtlcAttemptsForPayments(ctx context.Context, paymentIDs []int64) ([]sqlc.FetchHtlcAttemptsForPaymentsRow, error) + FetchHtlcAttemptResolutionsForPayments(ctx context.Context, paymentIDs []int64) ([]sqlc.FetchHtlcAttemptResolutionsForPaymentsRow, error) + FetchAllInflightAttempts(ctx context.Context, arg sqlc.FetchAllInflightAttemptsParams) ([]sqlc.PaymentHtlcAttempt, error) + FetchHopsForAttempts(ctx context.Context, htlcAttemptIndices []int64) ([]sqlc.FetchHopsForAttemptsRow, error) + + FetchAllPaymentDuplicates(ctx context.Context, + arg sqlc.FetchAllPaymentDuplicatesParams) ([]sqlc.PaymentDuplicate, error) + FetchPaymentDuplicates(ctx context.Context, paymentID int64) ([]sqlc.PaymentDuplicate, error) + + FetchPaymentLevelFirstHopCustomRecords(ctx context.Context, paymentIDs []int64) ([]sqlc.PaymentFirstHopCustomRecord, error) + FetchRouteLevelFirstHopCustomRecords(ctx context.Context, htlcAttemptIndices []int64) ([]sqlc.PaymentAttemptFirstHopCustomRecord, error) + FetchHopLevelCustomRecords(ctx context.Context, hopIDs []int64) ([]sqlc.PaymentHopCustomRecord, error) + + /* + Payment DB write operations. + */ + InsertPaymentIntent(ctx context.Context, arg sqlc.InsertPaymentIntentParams) (int64, error) + InsertPayment(ctx context.Context, arg sqlc.InsertPaymentParams) (int64, error) + InsertPaymentFirstHopCustomRecord(ctx context.Context, arg sqlc.InsertPaymentFirstHopCustomRecordParams) error + + InsertHtlcAttempt(ctx context.Context, arg sqlc.InsertHtlcAttemptParams) (int64, error) + InsertRouteHop(ctx context.Context, arg sqlc.InsertRouteHopParams) (int64, error) + InsertRouteHopMpp(ctx context.Context, arg sqlc.InsertRouteHopMppParams) error + InsertRouteHopAmp(ctx context.Context, arg sqlc.InsertRouteHopAmpParams) error + InsertRouteHopBlinded(ctx context.Context, arg sqlc.InsertRouteHopBlindedParams) error + + InsertPaymentAttemptFirstHopCustomRecord(ctx context.Context, arg sqlc.InsertPaymentAttemptFirstHopCustomRecordParams) error + InsertPaymentHopCustomRecord(ctx context.Context, arg sqlc.InsertPaymentHopCustomRecordParams) error + + SettleAttempt(ctx context.Context, arg sqlc.SettleAttemptParams) error + FailAttempt(ctx context.Context, arg sqlc.FailAttemptParams) error + + FailPayment(ctx context.Context, arg sqlc.FailPaymentParams) (sql.Result, error) + + DeletePayment(ctx context.Context, paymentID int64) error + + // DeleteFailedAttempts removes all failed HTLCs from the db for a + // given payment. + DeleteFailedAttempts(ctx context.Context, paymentID int64) error + + /* + Migration specific queries. + + These queries are used ONLY for the one-time migration from KV + to SQL. + */ + + // InsertPaymentMig is a migration-only variant of InsertPayment that + // allows setting fail_reason when inserting historical payments. + InsertPaymentMig(ctx context.Context, arg sqlc.InsertPaymentMigParams) (int64, error) + + // InsertPaymentDuplicateMig inserts a duplicate payment record during + // migration. + InsertPaymentDuplicateMig(ctx context.Context, arg sqlc.InsertPaymentDuplicateMigParams) (int64, error) +} + +// BatchedSQLQueries is a version of the SQLQueries that's capable +// of batched database operations. +type BatchedSQLQueries interface { + SQLQueries + sqldb.BatchedTx[SQLQueries] +} + +// SQLStore represents a storage backend. +type SQLStore struct { + cfg *SQLStoreConfig + db BatchedSQLQueries +} + +// A compile-time constraint to ensure SQLStore implements DB. +var _ DB = (*SQLStore)(nil) + +// SQLStoreConfig holds the configuration for the SQLStore. +type SQLStoreConfig struct { + // QueryConfig holds configuration values for SQL queries. + QueryCfg *sqldb.QueryConfig +} + +// NewSQLStore creates a new SQLStore instance given an open +// BatchedSQLPaymentsQueries storage backend. +func NewSQLStore(cfg *SQLStoreConfig, db BatchedSQLQueries, + options ...OptionModifier) (*SQLStore, error) { + + opts := DefaultOptions() + for _, applyOption := range options { + applyOption(opts) + } + + if opts.NoMigration { + return nil, fmt.Errorf("the NoMigration option is not yet " + + "supported for SQL stores") + } + + return &SQLStore{ + cfg: cfg, + db: db, + }, nil +} + +// A compile-time constraint to ensure SQLStore implements DB. +var _ DB = (*SQLStore)(nil) + +// fetchPaymentWithCompleteData fetches a payment with all its related data +// including attempts, hops, and custom records from the database. +// This is a convenience wrapper around the batch loading functions for single +// payment operations. +func fetchPaymentWithCompleteData(ctx context.Context, + cfg *sqldb.QueryConfig, db SQLQueries, + dbPayment sqlc.PaymentAndIntent) (*MPPayment, error) { + + payment := dbPayment.GetPayment() + + // Load batch data for this single payment. + batchData, err := batchLoadPaymentDetailsData( + ctx, cfg, db, []int64{payment.ID}, + ) + if err != nil { + return nil, fmt.Errorf("failed to load batch data: %w", err) + } + + // Build the payment from the batch data. + return buildPaymentFromBatchData(dbPayment, batchData) +} + +// paymentsCompleteData holds the full payment data when batch loading base +// payment data and all the related data for a payment. +type paymentsCompleteData struct { + *paymentsBaseData + *paymentsDetailsData +} + +// batchLoadPayments loads the full payment data for a batch of payment IDs. +func batchLoadPayments(ctx context.Context, cfg *sqldb.QueryConfig, + db SQLQueries, paymentIDs []int64) (*paymentsCompleteData, error) { + + baseData, err := batchLoadpaymentsBaseData(ctx, cfg, db, paymentIDs) + if err != nil { + return nil, fmt.Errorf("failed to load payment base data: %w", + err) + } + + batchData, err := batchLoadPaymentDetailsData(ctx, cfg, db, paymentIDs) + if err != nil { + return nil, fmt.Errorf("failed to load payment batch data: %w", + err) + } + + return &paymentsCompleteData{ + paymentsBaseData: baseData, + paymentsDetailsData: batchData, + }, nil +} + +// paymentsBaseData holds the base payment and intent data for a batch of +// payments. +type paymentsBaseData struct { + // paymentsAndIntents maps payment ID to its payment and intent data. + paymentsAndIntents map[int64]sqlc.PaymentAndIntent +} + +// batchLoadpaymentsBaseData loads the base payment and payment intent data for +// a batch of payment IDs. This complements loadPaymentsBatchData which loads +// related data (attempts, hops, custom records) but not the payment table +// and payment intent table data. +func batchLoadpaymentsBaseData(ctx context.Context, + cfg *sqldb.QueryConfig, db SQLQueries, + paymentIDs []int64) (*paymentsBaseData, error) { + + baseData := &paymentsBaseData{ + paymentsAndIntents: make(map[int64]sqlc.PaymentAndIntent), + } + + if len(paymentIDs) == 0 { + return baseData, nil + } + + err := sqldb.ExecuteBatchQuery( + ctx, cfg, paymentIDs, + func(id int64) int64 { return id }, + func(ctx context.Context, ids []int64) ( + []sqlc.FetchPaymentsByIDsRow, error) { + + records, err := db.FetchPaymentsByIDs( + ctx, ids, + ) + + return records, err + }, + func(ctx context.Context, + payment sqlc.FetchPaymentsByIDsRow) error { + + baseData.paymentsAndIntents[payment.ID] = payment + + return nil + }, + ) + if err != nil { + return nil, fmt.Errorf("failed to fetch payment base "+ + "data: %w", err) + } + + return baseData, nil +} + +// paymentsRelatedData holds all the batch-loaded data for multiple payments. +// This does not include the base payment and intent data which is fetched +// separately. It includes the additional data like attempts, hops, hop custom +// records, and route custom records. +type paymentsDetailsData struct { + // paymentCustomRecords maps payment ID to its custom records. + paymentCustomRecords map[int64][]sqlc.PaymentFirstHopCustomRecord + + // attempts maps payment ID to its HTLC attempts. + attempts map[int64][]sqlc.FetchHtlcAttemptsForPaymentsRow + + // hopsByAttempt maps attempt index to its hops. + hopsByAttempt map[int64][]sqlc.FetchHopsForAttemptsRow + + // hopCustomRecords maps hop ID to its custom records. + hopCustomRecords map[int64][]sqlc.PaymentHopCustomRecord + + // routeCustomRecords maps attempt index to its route-level custom + // records. + routeCustomRecords map[int64][]sqlc.PaymentAttemptFirstHopCustomRecord +} + +// batchLoadPaymentCustomRecords loads payment-level custom records for a given +// set of payment IDs. It uses a batch query to fetch all custom records for +// the given payment IDs. +func batchLoadPaymentCustomRecords(ctx context.Context, + cfg *sqldb.QueryConfig, db SQLQueries, paymentIDs []int64, + batchData *paymentsDetailsData) error { + + return sqldb.ExecuteBatchQuery( + ctx, cfg, paymentIDs, + func(id int64) int64 { return id }, + func(ctx context.Context, ids []int64) ( + []sqlc.PaymentFirstHopCustomRecord, error) { + + //nolint:ll + records, err := db.FetchPaymentLevelFirstHopCustomRecords( + ctx, ids, + ) + + return records, err + }, + func(ctx context.Context, + record sqlc.PaymentFirstHopCustomRecord) error { + + paymentRecords := + batchData.paymentCustomRecords[record.PaymentID] + + batchData.paymentCustomRecords[record.PaymentID] = + append(paymentRecords, record) + + return nil + }, + ) +} + +// batchLoadHtlcAttempts loads HTLC attempts for all payments and returns all +// attempt indices. It uses a batch query to fetch all attempts for the given +// payment IDs. +func batchLoadHtlcAttempts(ctx context.Context, cfg *sqldb.QueryConfig, + db SQLQueries, paymentIDs []int64, + batchData *paymentsDetailsData) ([]int64, error) { + + var allAttemptIndices []int64 + + err := sqldb.ExecuteBatchQuery( + ctx, cfg, paymentIDs, + func(id int64) int64 { return id }, + func(ctx context.Context, ids []int64) ( + []sqlc.FetchHtlcAttemptsForPaymentsRow, error) { + + return db.FetchHtlcAttemptsForPayments(ctx, ids) + }, + func(ctx context.Context, + attempt sqlc.FetchHtlcAttemptsForPaymentsRow) error { + + batchData.attempts[attempt.PaymentID] = append( + batchData.attempts[attempt.PaymentID], attempt, + ) + allAttemptIndices = append( + allAttemptIndices, attempt.AttemptIndex, + ) + + return nil + }, + ) + + return allAttemptIndices, err +} + +// batchLoadHopsForAttempts loads hops for all attempts and returns all hop IDs. +// It uses a batch query to fetch all hops for the given attempt indices. +func batchLoadHopsForAttempts(ctx context.Context, cfg *sqldb.QueryConfig, + db SQLQueries, attemptIndices []int64, + batchData *paymentsDetailsData) ([]int64, error) { + + var hopIDs []int64 + + err := sqldb.ExecuteBatchQuery( + ctx, cfg, attemptIndices, + func(idx int64) int64 { return idx }, + func(ctx context.Context, indices []int64) ( + []sqlc.FetchHopsForAttemptsRow, error) { + + return db.FetchHopsForAttempts(ctx, indices) + }, + func(ctx context.Context, + hop sqlc.FetchHopsForAttemptsRow) error { + + attemptHops := + batchData.hopsByAttempt[hop.HtlcAttemptIndex] + + batchData.hopsByAttempt[hop.HtlcAttemptIndex] = + append(attemptHops, hop) + + hopIDs = append(hopIDs, hop.ID) + + return nil + }, + ) + + return hopIDs, err +} + +// batchLoadHopCustomRecords loads hop-level custom records for all hops. It +// uses a batch query to fetch all custom records for the given hop IDs. +func batchLoadHopCustomRecords(ctx context.Context, cfg *sqldb.QueryConfig, + db SQLQueries, hopIDs []int64, batchData *paymentsDetailsData) error { + + return sqldb.ExecuteBatchQuery( + ctx, cfg, hopIDs, + func(id int64) int64 { return id }, + func(ctx context.Context, ids []int64) ( + []sqlc.PaymentHopCustomRecord, error) { + + return db.FetchHopLevelCustomRecords(ctx, ids) + }, + func(ctx context.Context, + record sqlc.PaymentHopCustomRecord) error { + + // TODO(ziggie): Can we get rid of this? + // This has to be in place otherwise the + // comparison will not match. + if record.Value == nil { + record.Value = []byte{} + } + + batchData.hopCustomRecords[record.HopID] = append( + batchData.hopCustomRecords[record.HopID], + record, + ) + + return nil + }, + ) +} + +// batchLoadRouteCustomRecords loads route-level first hop custom records for +// all attempts. It uses a batch query to fetch all custom records for the given +// attempt indices. +func batchLoadRouteCustomRecords(ctx context.Context, cfg *sqldb.QueryConfig, + db SQLQueries, attemptIndices []int64, + batchData *paymentsDetailsData) error { + + return sqldb.ExecuteBatchQuery( + ctx, cfg, attemptIndices, + func(idx int64) int64 { return idx }, + func(ctx context.Context, indices []int64) ( + []sqlc.PaymentAttemptFirstHopCustomRecord, error) { + + return db.FetchRouteLevelFirstHopCustomRecords( + ctx, indices, + ) + }, + func(ctx context.Context, + record sqlc.PaymentAttemptFirstHopCustomRecord) error { + + idx := record.HtlcAttemptIndex + attemptRecords := batchData.routeCustomRecords[idx] + + batchData.routeCustomRecords[idx] = + append(attemptRecords, record) + + return nil + }, + ) +} + +// paymentStatusData holds lightweight resolution data for computing +// payment status efficiently during deletion operations. +type paymentStatusData struct { + // resolutionTypes maps payment ID to a list of resolution types + // for that payment's HTLC attempts. + resolutionTypes map[int64][]sql.NullInt32 +} + +// batchLoadPaymentResolutions loads only HTLC resolution types for multiple +// payments. This is a lightweight alternative to batchLoadPaymentsRelatedData +// that's optimized for operations that only need to determine payment status. +func batchLoadPaymentResolutions(ctx context.Context, cfg *sqldb.QueryConfig, + db SQLQueries, paymentIDs []int64) (*paymentStatusData, error) { + + batchStatusData := &paymentStatusData{ + resolutionTypes: make(map[int64][]sql.NullInt32), + } + + if len(paymentIDs) == 0 { + return batchStatusData, nil + } + + // Use a batch query to fetch all resolution types for the given payment + // IDs. + err := sqldb.ExecuteBatchQuery( + ctx, cfg, paymentIDs, + func(id int64) int64 { return id }, + func(ctx context.Context, ids []int64) ( + []sqlc.FetchHtlcAttemptResolutionsForPaymentsRow, + error) { + + return db.FetchHtlcAttemptResolutionsForPayments( + ctx, ids, + ) + }, + //nolint:ll + func(ctx context.Context, + res sqlc.FetchHtlcAttemptResolutionsForPaymentsRow) error { + + // Group resolutions by payment ID. + batchStatusData.resolutionTypes[res.PaymentID] = append( + batchStatusData.resolutionTypes[res.PaymentID], + res.ResolutionType, + ) + + return nil + }, + ) + if err != nil { + return nil, fmt.Errorf("failed to fetch HTLC resolutions: %w", + err) + } + + return batchStatusData, nil +} + +// loadPaymentResolutions is a single-payment wrapper around +// batchLoadPaymentResolutions for convenience and to prevent duplicate queries +// so we reuse the same batch query for all payments. +func loadPaymentResolutions(ctx context.Context, cfg *sqldb.QueryConfig, + db SQLQueries, paymentID int64) ([]sql.NullInt32, error) { + + batchData, err := batchLoadPaymentResolutions( + ctx, cfg, db, []int64{paymentID}, + ) + if err != nil { + return nil, err + } + + return batchData.resolutionTypes[paymentID], nil +} + +// computePaymentStatusFromResolutions determines the payment status from +// resolution types and failure reason without building the complete MPPayment +// structure. This is a lightweight version that builds minimal HTLCAttempt +// structures and delegates to decidePaymentStatus for consistency. +func computePaymentStatusFromResolutions(resolutionTypes []sql.NullInt32, + failReason sql.NullInt32) (PaymentStatus, error) { + + // Build minimal HTLCAttempt slice with only resolution info. + htlcs := make([]HTLCAttempt, len(resolutionTypes)) + for i, resType := range resolutionTypes { + if !resType.Valid { + // NULL resolution_type means in-flight (no Settle, no + // Failure). + continue + } + + switch HTLCAttemptResolutionType(resType.Int32) { + case HTLCAttemptResolutionSettled: + // Mark as settled (preimage details not needed for + // status). + htlcs[i].Settle = &HTLCSettleInfo{} + + case HTLCAttemptResolutionFailed: + // Mark as failed (failure details not needed for + // status). + htlcs[i].Failure = &HTLCFailInfo{} + + default: + return 0, fmt.Errorf("unknown resolution type: %v", + resType.Int32) + } + } + + // Convert fail reason to FailureReason pointer. + var failureReason *FailureReason + if failReason.Valid { + reason := FailureReason(failReason.Int32) + failureReason = &reason + } + + // Use the existing status decision logic. + return decidePaymentStatus(htlcs, failureReason) +} + +// batchLoadPaymentDetailsData loads all related data for multiple payments in +// batch. It uses a batch queries to fetch all data for the given payment IDs. +func batchLoadPaymentDetailsData(ctx context.Context, cfg *sqldb.QueryConfig, + db SQLQueries, paymentIDs []int64) (*paymentsDetailsData, error) { + + batchData := &paymentsDetailsData{ + paymentCustomRecords: make( + map[int64][]sqlc.PaymentFirstHopCustomRecord, + ), + attempts: make( + map[int64][]sqlc.FetchHtlcAttemptsForPaymentsRow, + ), + hopsByAttempt: make( + map[int64][]sqlc.FetchHopsForAttemptsRow, + ), + hopCustomRecords: make( + map[int64][]sqlc.PaymentHopCustomRecord, + ), + routeCustomRecords: make( + map[int64][]sqlc.PaymentAttemptFirstHopCustomRecord, + ), + } + + if len(paymentIDs) == 0 { + return batchData, nil + } + + // Load payment-level custom records. + err := batchLoadPaymentCustomRecords( + ctx, cfg, db, paymentIDs, batchData, + ) + if err != nil { + return nil, fmt.Errorf("failed to fetch payment custom "+ + "records: %w", err) + } + + // Load HTLC attempts and collect attempt indices. + allAttemptIndices, err := batchLoadHtlcAttempts( + ctx, cfg, db, paymentIDs, batchData, + ) + if err != nil { + return nil, fmt.Errorf("failed to fetch HTLC attempts: %w", + err) + } + + if len(allAttemptIndices) == 0 { + // No attempts, return early. + return batchData, nil + } + + // Load hops for all attempts and collect hop IDs. + hopIDs, err := batchLoadHopsForAttempts( + ctx, cfg, db, allAttemptIndices, batchData, + ) + if err != nil { + return nil, fmt.Errorf("failed to fetch hops for attempts: %w", + err) + } + + // Load hop-level custom records if there are any hops. + if len(hopIDs) > 0 { + err = batchLoadHopCustomRecords(ctx, cfg, db, hopIDs, batchData) + if err != nil { + return nil, fmt.Errorf("failed to fetch hop custom "+ + "records: %w", err) + } + } + + // Load route-level first hop custom records. + err = batchLoadRouteCustomRecords( + ctx, cfg, db, allAttemptIndices, batchData, + ) + if err != nil { + return nil, fmt.Errorf("failed to fetch route custom "+ + "records: %w", err) + } + + return batchData, nil +} + +// buildPaymentFromBatchData builds a complete MPPayment from a database payment +// and pre-loaded batch data. +func buildPaymentFromBatchData(dbPayment sqlc.PaymentAndIntent, + batchData *paymentsDetailsData) (*MPPayment, error) { + + // The query will only return BOLT 11 payment intents or intents with + // no intent type set. + paymentIntent := dbPayment.GetPaymentIntent() + paymentRequest := paymentIntent.IntentPayload + + payment := dbPayment.GetPayment() + + // Get payment-level custom records from batch data. + customRecords := batchData.paymentCustomRecords[payment.ID] + + // Convert to the FirstHopCustomRecords map. + var firstHopCustomRecords lnwire.CustomRecords + if len(customRecords) > 0 { + firstHopCustomRecords = make(lnwire.CustomRecords) + for _, record := range customRecords { + firstHopCustomRecords[uint64(record.Key)] = record.Value + } + } + + // Convert database payment data to the PaymentCreationInfo struct. + info := dbPaymentToCreationInfo( + payment.PaymentIdentifier, payment.AmountMsat, + payment.CreatedAt, paymentRequest, firstHopCustomRecords, + ) + + // Get all HTLC attempts from batch data for a given payment. + dbAttempts := batchData.attempts[payment.ID] + + // Convert all attempts to HTLCAttempt structs using the pre-loaded + // batch data. + attempts := make([]HTLCAttempt, 0, len(dbAttempts)) + for _, dbAttempt := range dbAttempts { + attemptIndex := dbAttempt.AttemptIndex + // Convert the batch row type to the single row type. + attempt, err := dbAttemptToHTLCAttempt( + dbAttempt, batchData.hopsByAttempt[attemptIndex], + batchData.hopCustomRecords, + batchData.routeCustomRecords[attemptIndex], + ) + if err != nil { + return nil, fmt.Errorf("failed to convert attempt "+ + "%d: %w", attemptIndex, err) + } + attempts = append(attempts, *attempt) + } + + // Set the failure reason if present. + // + // TODO(ziggie): Rename it to Payment Memo in the database? + var failureReason *FailureReason + if payment.FailReason.Valid { + reason := FailureReason(payment.FailReason.Int32) + failureReason = &reason + } + + mpPayment := &MPPayment{ + SequenceNum: uint64(payment.ID), + Info: info, + HTLCs: attempts, + FailureReason: failureReason, + } + + // The status and state will be determined by calling + // SetState after construction. + if err := mpPayment.SetState(); err != nil { + return nil, fmt.Errorf("failed to set payment state: %w", err) + } + + return mpPayment, nil +} + +// QueryPayments queries and retrieves payments from the database with support +// for filtering, pagination, and efficient batch loading of related data. +// +// The function accepts a Query parameter that controls: +// - Pagination: IndexOffset specifies where to start (exclusive), and +// MaxPayments limits the number of results returned +// - Ordering: Reversed flag determines if results are returned in reverse +// chronological order +// - Filtering: CreationDateStart/End filter by creation time, and +// IncludeIncomplete controls whether non-succeeded payments are included +// - Metadata: CountTotal flag determines if the total payment count should +// be calculated +// +// The function optimizes performance by loading all related data (HTLCs, +// sequences, failure reasons, etc.) for multiple payments in a single batch +// query, rather than fetching each payment's data individually. +// +// Returns a Response containing: +// - Payments: the list of matching payments with complete data +// - FirstIndexOffset/LastIndexOffset: pagination cursors for the first and +// last payment in the result set +// - TotalCount: total number of payments in the database (if CountTotal was +// requested, otherwise 0) +// +// This is part of the DB interface. +func (s *SQLStore) QueryPayments(ctx context.Context, query Query) (Response, + error) { + + if query.MaxPayments == 0 { + return Response{}, fmt.Errorf("max payments must be non-zero") + } + + var ( + allPayments []*MPPayment + totalCount int64 + initialCursor int64 + ) + + extractCursor := func(row sqlc.FilterPaymentsRow) int64 { + return row.Payment.ID + } + + err := s.db.ExecTx(ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error { + // We first count all payments to determine the total count + // if requested. + if query.CountTotal { + totalPayments, err := db.CountPayments(ctx) + if err != nil { + return fmt.Errorf("failed to count "+ + "payments: %w", err) + } + totalCount = totalPayments + } + + // collectFunc extracts the payment ID from each payment row. + collectFunc := func(row sqlc.FilterPaymentsRow) (int64, error) { + return row.Payment.ID, nil + } + + // batchDataFunc loads all related data for a batch of payments. + batchDataFunc := func(ctx context.Context, paymentIDs []int64) ( + *paymentsDetailsData, error) { + + return batchLoadPaymentDetailsData( + ctx, s.cfg.QueryCfg, db, paymentIDs, + ) + } + + // processPayment processes each payment with the batch-loaded + // data. + processPayment := func(ctx context.Context, + dbPayment sqlc.FilterPaymentsRow, + batchData *paymentsDetailsData) error { + + // Build the payment from the pre-loaded batch data. + mpPayment, err := buildPaymentFromBatchData( + dbPayment, batchData, + ) + if err != nil { + return fmt.Errorf("failed to fetch payment "+ + "with complete data: %w", err) + } + + // To keep compatibility with the old API, we only + // return non-succeeded payments if requested. + if mpPayment.Status != StatusSucceeded && + !query.IncludeIncomplete { + + return nil + } + + if uint64(len(allPayments)) >= query.MaxPayments { + return errMaxPaymentsReached + } + + allPayments = append(allPayments, mpPayment) + + return nil + } + + queryFunc := func(ctx context.Context, lastID int64, + limit int32) ([]sqlc.FilterPaymentsRow, error) { + + filterParams := sqlc.FilterPaymentsParams{ + NumLimit: limit, + Reverse: query.Reversed, + // For now there only BOLT 11 payment intents + // exist. + IntentType: sqldb.SQLInt16( + PaymentIntentTypeBolt11, + ), + } + + if query.Reversed { + filterParams.IndexOffsetLet = sqldb.SQLInt64( + lastID, + ) + } else { + filterParams.IndexOffsetGet = sqldb.SQLInt64( + lastID, + ) + } + + // Add potential date filters if specified. + if query.CreationDateStart != 0 { + filterParams.CreatedAfter = sqldb.SQLTime( + time.Unix(query.CreationDateStart, 0). + UTC(), + ) + } + if query.CreationDateEnd != 0 { + filterParams.CreatedBefore = sqldb.SQLTime( + time.Unix(query.CreationDateEnd, 0). + UTC(), + ) + } + + return db.FilterPayments(ctx, filterParams) + } + + if query.Reversed { + if query.IndexOffset == 0 { + initialCursor = int64(math.MaxInt64) + } else { + initialCursor = int64(query.IndexOffset) + } + } else { + initialCursor = int64(query.IndexOffset) + } + + return sqldb.ExecuteCollectAndBatchWithSharedDataQuery( + ctx, s.cfg.QueryCfg, initialCursor, queryFunc, + extractCursor, collectFunc, batchDataFunc, + processPayment, + ) + }, func() { + allPayments = nil + }) + + // We make sure we don't return an error if we reached the maximum + // number of payments. Which is the pagination limit for the query + // itself. + if err != nil && !errors.Is(err, errMaxPaymentsReached) { + return Response{}, fmt.Errorf("failed to query payments: %w", + err) + } + + // Handle case where no payments were found + if len(allPayments) == 0 { + return Response{ + Payments: allPayments, + FirstIndexOffset: 0, + LastIndexOffset: 0, + TotalCount: uint64(totalCount), + }, nil + } + + // If the query was reversed, we need to reverse the payment list + // to match the kvstore behavior and return payments in forward order. + if query.Reversed { + for i, j := 0, len(allPayments)-1; i < j; i, j = i+1, j-1 { + allPayments[i], allPayments[j] = allPayments[j], + allPayments[i] + } + } + + return Response{ + Payments: allPayments, + FirstIndexOffset: allPayments[0].SequenceNum, + LastIndexOffset: allPayments[len(allPayments)-1].SequenceNum, + TotalCount: uint64(totalCount), + }, nil +} + +// fetchPaymentByHash fetches a payment by its hash from the database. It is a +// convenience wrapper around the FetchPayment method and checks for +// no rows error and returns ErrPaymentNotInitiated if no payment is found. +func fetchPaymentByHash(ctx context.Context, db SQLQueries, + paymentHash lntypes.Hash) (sqlc.FetchPaymentRow, error) { + + dbPayment, err := db.FetchPayment(ctx, paymentHash[:]) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return dbPayment, fmt.Errorf("failed to fetch payment: %w", err) + } + + if errors.Is(err, sql.ErrNoRows) { + return dbPayment, ErrPaymentNotInitiated + } + + return dbPayment, nil +} + +// FetchPayment retrieves a complete payment record from the database by its +// payment hash. The returned MPPayment includes all payment metadata such as +// creation info, payment status, current state, all HTLC attempts (both +// successful and failed), and the failure reason if the payment has been +// marked as failed. +// +// Returns ErrPaymentNotInitiated if no payment with the given hash exists. +// +// This is part of the DB interface. +func (s *SQLStore) FetchPayment(ctx context.Context, + paymentHash lntypes.Hash) (*MPPayment, error) { + + var mpPayment *MPPayment + + err := s.db.ExecTx(ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error { + dbPayment, err := fetchPaymentByHash(ctx, db, paymentHash) + if err != nil { + return err + } + + mpPayment, err = fetchPaymentWithCompleteData( + ctx, s.cfg.QueryCfg, db, dbPayment, + ) + if err != nil { + return fmt.Errorf("failed to fetch payment with "+ + "complete data: %w", err) + } + + return nil + }, sqldb.NoOpReset) + if err != nil { + return nil, err + } + + return mpPayment, nil +} + +// FetchDuplicatePayments retrieves duplicate payment records for a payment +// hash from the SQL store. +func (s *SQLStore) FetchDuplicatePayments(ctx context.Context, + paymentHash lntypes.Hash) ([]*DuplicatePayment, error) { + + var duplicates []*DuplicatePayment + + err := s.db.ExecTx(ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error { + dbPayment, err := fetchPaymentByHash(ctx, db, paymentHash) + if err != nil { + return err + } + + rows, err := db.FetchPaymentDuplicates( + ctx, dbPayment.Payment.ID, + ) + if err != nil { + return err + } + + duplicates, err = dbDuplicatePaymentsToDuplicatePayments(rows) + return err + }, sqldb.NoOpReset) + if err != nil { + return nil, err + } + + return duplicates, nil +} + +// FetchAllDuplicatePayments retrieves duplicate payment records ordered by +// payment_id and id using cursor-based pagination. +func (s *SQLStore) FetchAllDuplicatePayments(ctx context.Context) ( + []*DuplicatePayment, error) { + + type duplicateCursor struct { + paymentID int64 + id int64 + } + + var ( + rows []sqlc.PaymentDuplicate + ) + + //nolint:ll + err := s.db.ExecTx(ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error { + return sqldb.ExecutePaginatedQuery( + ctx, s.cfg.QueryCfg, duplicateCursor{}, + func(ctx context.Context, cursor duplicateCursor, + pageSize int32) ([]sqlc.PaymentDuplicate, + error) { + + return db.FetchAllPaymentDuplicates( + ctx, + sqlc.FetchAllPaymentDuplicatesParams{ + AfterPaymentID: cursor.paymentID, + AfterID: cursor.id, + NumLimit: pageSize, + }, + ) + }, + func(row sqlc.PaymentDuplicate) duplicateCursor { + return duplicateCursor{ + paymentID: row.PaymentID, + id: row.ID, + } + }, + func(_ context.Context, + row sqlc.PaymentDuplicate) error { + + rows = append(rows, row) + return nil + }, + ) + }, sqldb.NoOpReset) + if err != nil { + return nil, err + } + + duplicates, err := dbDuplicatePaymentsToDuplicatePayments(rows) + if err != nil { + return nil, err + } + + return duplicates, nil +} + +// FetchInFlightPayments retrieves all payments that have HTLC attempts +// currently in flight (not yet settled or failed). These are payments with at +// least one HTLC attempt that has been registered but has no resolution record. +// +// The SQLStore implementation provides a significant performance improvement +// over the KVStore implementation by using targeted SQL queries instead of +// scanning all payments. +// +// This method is part of the PaymentReader interface, which is embedded in the +// DB interface. It's typically called during node startup to resume monitoring +// of pending payments and ensure HTLCs are properly tracked. +// +// TODO(ziggie): Consider changing the interface to use a callback or iterator +// pattern instead of returning all payments at once. This would allow +// processing payments one at a time without holding them all in memory +// simultaneously: +// - Callback: func FetchInFlightPayments(ctx, func(*MPPayment) error) error +// - Iterator: func FetchInFlightPayments(ctx) (PaymentIterator, error) +// +// While inflight payments are typically a small subset, this would improve +// memory efficiency for nodes with unusually high numbers of concurrent +// payments and would better leverage the existing pagination infrastructure. +func (s *SQLStore) FetchInFlightPayments(ctx context.Context) ([]*MPPayment, + error) { + + var mpPayments []*MPPayment + + err := s.db.ExecTx(ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error { + // Track which payment IDs we've already processed across all + // pages to avoid loading the same payment multiple times when + // multiple inflight attempts belong to the same payment. + processedPayments := make(map[int64]*MPPayment) + + extractCursor := func(row sqlc.PaymentHtlcAttempt) int64 { + return row.AttemptIndex + } + + // collectFunc extracts the payment ID from each attempt row. + collectFunc := func(row sqlc.PaymentHtlcAttempt) ( + int64, error) { + + return row.PaymentID, nil + } + + // batchDataFunc loads payment data for a batch of payment IDs, + // but only for IDs we haven't processed yet. + batchDataFunc := func(ctx context.Context, + paymentIDs []int64) (*paymentsCompleteData, error) { + + // Filter out already-processed payment IDs. + uniqueIDs := make([]int64, 0, len(paymentIDs)) + for _, id := range paymentIDs { + _, processed := processedPayments[id] + if !processed { + uniqueIDs = append(uniqueIDs, id) + } + } + + // If uniqueIDs is empty, the batch load will return + // empty batch data. + return batchLoadPayments( + ctx, s.cfg.QueryCfg, db, uniqueIDs, + ) + } + + // processAttempt processes each attempt. We only build and + // store the payment once per unique payment ID. + processAttempt := func(ctx context.Context, + row sqlc.PaymentHtlcAttempt, + batchData *paymentsCompleteData) error { + + // Skip if we've already processed this payment. + _, processed := processedPayments[row.PaymentID] + if processed { + return nil + } + + dbPayment := batchData.paymentsAndIntents[row.PaymentID] + + // Build the payment from batch data. + mpPayment, err := buildPaymentFromBatchData( + dbPayment, batchData.paymentsDetailsData, + ) + if err != nil { + return fmt.Errorf("failed to build payment: %w", + err) + } + + // Store in our processed map. + processedPayments[row.PaymentID] = mpPayment + + return nil + } + + queryFunc := func(ctx context.Context, lastAttemptIndex int64, + limit int32) ([]sqlc.PaymentHtlcAttempt, + error) { + + return db.FetchAllInflightAttempts(ctx, + sqlc.FetchAllInflightAttemptsParams{ + AttemptIndex: lastAttemptIndex, + Limit: limit, + }, + ) + } + + err := sqldb.ExecuteCollectAndBatchWithSharedDataQuery( + ctx, s.cfg.QueryCfg, int64(-1), queryFunc, + extractCursor, collectFunc, batchDataFunc, + processAttempt, + ) + if err != nil { + return err + } + + // Convert map to slice. + mpPayments = make([]*MPPayment, 0, len(processedPayments)) + for _, payment := range processedPayments { + mpPayments = append(mpPayments, payment) + } + + return nil + }, func() { + mpPayments = nil + }) + if err != nil { + return nil, fmt.Errorf("failed to fetch inflight "+ + "payments: %w", err) + } + + return mpPayments, nil +} + +// DeleteFailedAttempts removes all failed HTLC attempts from the database for +// the specified payment, while preserving the payment record itself and any +// successful or in-flight attempts. +// +// The method performs the following validations before deletion: +// - StatusInitiated: Can delete failed attempts +// - StatusInFlight: Cannot delete, returns ErrPaymentInFlight (active HTLCs +// still on the network) +// - StatusSucceeded: Can delete failed attempts (payment completed) +// - StatusFailed: Can delete failed attempts (payment permanently failed) +// +// This method is idempotent - calling it multiple times on the same payment +// has no adverse effects. +// +// This method is part of the PaymentControl interface, which is embedded in +// the PaymentWriter interface and ultimately the DB interface. It represents +// the final step (step 5) in the payment lifecycle control flow and should be +// called after a payment reaches a terminal state (succeeded or permanently +// failed) to clean up historical failed attempts. +func (s *SQLStore) DeleteFailedAttempts(ctx context.Context, + paymentHash lntypes.Hash) error { + + err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error { + dbPayment, err := fetchPaymentByHash(ctx, db, paymentHash) + if err != nil { + return err + } + + paymentStatus, err := computePaymentStatusFromDB( + ctx, s.cfg.QueryCfg, db, dbPayment, + ) + if err != nil { + return fmt.Errorf("failed to compute payment "+ + "status: %w", err) + } + + if err := paymentStatus.removable(); err != nil { + return fmt.Errorf("cannot delete failed "+ + "attempts for payment %v: %w", paymentHash, err) + } + + // Then we delete the failed attempts for this payment. + return db.DeleteFailedAttempts(ctx, dbPayment.GetPayment().ID) + }, sqldb.NoOpReset) + if err != nil { + return fmt.Errorf("failed to delete failed attempts for "+ + "payment %v: %w", paymentHash, err) + } + + return nil +} + +// computePaymentStatusFromDB computes the payment status by fetching minimal +// data from the database. This is a lightweight query optimized for SQL that +// doesn't load route data, making it significantly more efficient than +// FetchPayment when only the status is needed. +func computePaymentStatusFromDB(ctx context.Context, cfg *sqldb.QueryConfig, + db SQLQueries, dbPayment sqlc.PaymentAndIntent) (PaymentStatus, error) { + + payment := dbPayment.GetPayment() + + // Load the resolution types for the payment. + resolutionTypes, err := loadPaymentResolutions( + ctx, cfg, db, payment.ID, + ) + if err != nil { + return 0, fmt.Errorf("failed to load payment resolutions: %w", + err) + } + + // Use the lightweight status computation. + status, err := computePaymentStatusFromResolutions( + resolutionTypes, payment.FailReason, + ) + if err != nil { + return 0, fmt.Errorf("failed to compute payment status: %w", + err) + } + + return status, nil +} + +// DeletePayment removes a payment or its failed HTLC attempts from the +// database based on the failedAttemptsOnly flag. +// +// If failedAttemptsOnly is true, this method deletes only the failed HTLC +// attempts for the payment while preserving the payment record itself and any +// successful or in-flight attempts. This is useful for cleaning up historical +// failed attempts after a payment reaches a terminal state. +// +// If failedAttemptsOnly is false, this method deletes the entire payment +// record including all payment metadata, payment creation info, all HTLC +// attempts (both failed and successful), and associated data such as payment +// intents and custom records. +// +// Before deletion, this method validates the payment status to ensure it's +// safe to delete: +// - StatusInitiated: Can be deleted (no HTLCs sent yet) +// - StatusInFlight: Cannot be deleted, returns ErrPaymentInFlight (active +// HTLCs on the network) +// - StatusSucceeded: Can be deleted (payment completed successfully) +// - StatusFailed: Can be deleted (payment has failed permanently) +// +// Returns an error if the payment has in-flight HTLCs or if the payment +// doesn't exist. +// +// This method is part of the PaymentWriter interface, which is embedded in +// the DB interface. +func (s *SQLStore) DeletePayment(ctx context.Context, paymentHash lntypes.Hash, + failedHtlcsOnly bool) error { + + err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error { + dbPayment, err := fetchPaymentByHash(ctx, db, paymentHash) + if err != nil { + return err + } + + paymentStatus, err := computePaymentStatusFromDB( + ctx, s.cfg.QueryCfg, db, dbPayment, + ) + if err != nil { + return fmt.Errorf("failed to compute payment "+ + "status: %w", err) + } + + if err := paymentStatus.removable(); err != nil { + return fmt.Errorf("payment %v cannot be deleted: %w", + paymentHash, err) + } + + // If we are only deleting failed HTLCs, we delete them. + if failedHtlcsOnly { + return db.DeleteFailedAttempts( + ctx, dbPayment.GetPayment().ID, + ) + } + + // In case we are not deleting failed HTLCs, we delete the + // payment which will cascade delete all related data. + return db.DeletePayment(ctx, dbPayment.GetPayment().ID) + }, sqldb.NoOpReset) + if err != nil { + return fmt.Errorf("failed to delete failed attempts for "+ + "payment %v: %w", paymentHash, err) + } + + return nil +} + +// InitPayment creates a new payment record in the database with the given +// payment hash and creation info. +// +// Before creating the payment, this method checks if a payment with the same +// hash already exists and validates whether initialization is allowed based on +// the existing payment's status: +// - StatusInitiated: Returns ErrPaymentExists (payment already created, +// HTLCs may be in flight) +// - StatusInFlight: Returns ErrPaymentInFlight (payment currently being +// attempted) +// - StatusSucceeded: Returns ErrAlreadyPaid (payment already succeeded) +// - StatusFailed: Allows retry by deleting the old payment record and +// creating a new one +// +// If no existing payment is found, a new payment record is created with +// StatusInitiated and stored with all associated metadata. +// +// This method is part of the PaymentControl interface, which is embedded in +// the PaymentWriter interface and ultimately the DB interface, representing +// the first step in the payment lifecycle control flow. +func (s *SQLStore) InitPayment(ctx context.Context, paymentHash lntypes.Hash, + paymentCreationInfo *PaymentCreationInfo) error { + + // Create the payment in the database. + err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error { + existingPayment, err := db.FetchPayment(ctx, paymentHash[:]) + switch { + // A payment with this hash already exists. We need to check its + // status to see if we can re-initialize. + case err == nil: + paymentStatus, err := computePaymentStatusFromDB( + ctx, s.cfg.QueryCfg, db, existingPayment, + ) + if err != nil { + return fmt.Errorf("failed to compute payment "+ + "status: %w", err) + } + + // Check if the payment is initializable otherwise + // we'll return early. + if err := paymentStatus.initializable(); err != nil { + return fmt.Errorf("payment is not "+ + "initializable: %w", err) + } + + // If the initializable check above passes, then the + // existing payment has failed. So we delete it and + // all of its previous artifacts. We rely on + // cascading deletes to clean up the rest. + err = db.DeletePayment(ctx, existingPayment.Payment.ID) + if err != nil { + return fmt.Errorf("failed to delete "+ + "payment: %w", err) + } + + // An unexpected error occurred while fetching the payment. + case !errors.Is(err, sql.ErrNoRows): + // Some other error occurred + return fmt.Errorf("failed to check existing "+ + "payment: %w", err) + + // The payment does not yet exist, so we can proceed. + default: + } + + // Insert the payment first to get its ID. + paymentID, err := db.InsertPayment( + ctx, sqlc.InsertPaymentParams{ + AmountMsat: int64( + paymentCreationInfo.Value, + ), + CreatedAt: paymentCreationInfo. + CreationTime.UTC(), + PaymentIdentifier: paymentHash[:], + }, + ) + if err != nil { + return fmt.Errorf("failed to insert payment: %w", err) + } + + // If there's a payment request, insert the payment intent. + if len(paymentCreationInfo.PaymentRequest) > 0 { + _, err = db.InsertPaymentIntent( + ctx, sqlc.InsertPaymentIntentParams{ + PaymentID: paymentID, + IntentType: int16( + PaymentIntentTypeBolt11, + ), + IntentPayload: paymentCreationInfo. + PaymentRequest, + }, + ) + if err != nil { + return fmt.Errorf("failed to insert "+ + "payment intent: %w", err) + } + } + + firstHopCustomRecords := paymentCreationInfo. + FirstHopCustomRecords + + for key, value := range firstHopCustomRecords { + err = db.InsertPaymentFirstHopCustomRecord( + ctx, + sqlc.InsertPaymentFirstHopCustomRecordParams{ + PaymentID: paymentID, + Key: int64(key), + Value: value, + }, + ) + if err != nil { + return fmt.Errorf("failed to insert "+ + "payment first hop custom "+ + "record: %w", err) + } + } + + return nil + }, sqldb.NoOpReset) + if err != nil { + return fmt.Errorf("failed to initialize payment: %w", err) + } + + return nil +} + +// insertRouteHops inserts all route hop data for a given set of hops. +func (s *SQLStore) insertRouteHops(ctx context.Context, db SQLQueries, + hops []*route.Hop, attemptID uint64) error { + + for i, hop := range hops { + // Insert the basic route hop data and get the generated ID. + hopID, err := db.InsertRouteHop(ctx, sqlc.InsertRouteHopParams{ + HtlcAttemptIndex: int64(attemptID), + HopIndex: int32(i), + PubKey: hop.PubKeyBytes[:], + Scid: strconv.FormatUint( + hop.ChannelID, 10, + ), + OutgoingTimeLock: int32(hop.OutgoingTimeLock), + AmtToForward: int64(hop.AmtToForward), + MetaData: hop.Metadata, + }) + if err != nil { + return fmt.Errorf("failed to insert route hop: %w", err) + } + + // Insert the per-hop custom records. + if len(hop.CustomRecords) > 0 { + for key, value := range hop.CustomRecords { + err = db.InsertPaymentHopCustomRecord( + ctx, + sqlc.InsertPaymentHopCustomRecordParams{ + HopID: hopID, + Key: int64(key), + Value: value, + }) + if err != nil { + return fmt.Errorf("failed to insert "+ + "payment hop custom record: %w", + err) + } + } + } + + // Insert MPP data if present. + if hop.MPP != nil { + paymentAddr := hop.MPP.PaymentAddr() + err = db.InsertRouteHopMpp( + ctx, sqlc.InsertRouteHopMppParams{ + HopID: hopID, + PaymentAddr: paymentAddr[:], + TotalMsat: int64(hop.MPP.TotalMsat()), + }) + if err != nil { + return fmt.Errorf("failed to insert "+ + "route hop MPP: %w", err) + } + } + + // Insert AMP data if present. + if hop.AMP != nil { + rootShare := hop.AMP.RootShare() + setID := hop.AMP.SetID() + err = db.InsertRouteHopAmp( + ctx, sqlc.InsertRouteHopAmpParams{ + HopID: hopID, + RootShare: rootShare[:], + SetID: setID[:], + ChildIndex: int32(hop.AMP.ChildIndex()), + }) + if err != nil { + return fmt.Errorf("failed to insert "+ + "route hop AMP: %w", err) + } + } + + // Insert blinded route data if present. Every hop in the + // blinded path must have an encrypted data record. If the + // encrypted data is not present, we skip the insertion. + if hop.EncryptedData == nil { + continue + } + + // The introduction point has a blinding point set. + var blindingPointBytes []byte + if hop.BlindingPoint != nil { + blindingPointBytes = hop.BlindingPoint. + SerializeCompressed() + } + + // The total amount is only set for the final hop in a + // blinded path. + totalAmtMsat := sql.NullInt64{} + if i == len(hops)-1 { + totalAmtMsat = sql.NullInt64{ + Int64: int64(hop.TotalAmtMsat), + Valid: true, + } + } + + err = db.InsertRouteHopBlinded(ctx, + sqlc.InsertRouteHopBlindedParams{ + HopID: hopID, + EncryptedData: hop.EncryptedData, + BlindingPoint: blindingPointBytes, + BlindedPathTotalAmt: totalAmtMsat, + }, + ) + if err != nil { + return fmt.Errorf("failed to insert "+ + "route hop blinded: %w", err) + } + } + + return nil +} + +// RegisterAttempt atomically records a new HTLC attempt for the specified +// payment. The attempt includes the attempt ID, session key, route information +// (hops, timelocks, amounts), and optional data such as MPP/AMP parameters, +// blinded route data, and custom records. +// +// Returns the updated MPPayment with the new attempt appended to the HTLCs +// slice, and the payment state recalculated. Returns an error if the payment +// doesn't exist or validation fails. +// +// This method is part of the PaymentControl interface, which is embedded in +// the PaymentWriter interface and ultimately the DB interface. It represents +// step 2 in the payment lifecycle control flow, called after InitPayment and +// potentially multiple times for multi-path payments. +func (s *SQLStore) RegisterAttempt(ctx context.Context, + paymentHash lntypes.Hash, attempt *HTLCAttemptInfo) (*MPPayment, + error) { + + var mpPayment *MPPayment + + err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error { + // Make sure the payment exists. + dbPayment, err := db.FetchPayment(ctx, paymentHash[:]) + if err != nil { + return err + } + + // We fetch the complete payment to determine if the payment is + // registrable. + // + // TODO(ziggie): We could improve the query here since only + // the last hop data is needed here not the complete payment + // data. + mpPayment, err = fetchPaymentWithCompleteData( + ctx, s.cfg.QueryCfg, db, dbPayment, + ) + if err != nil { + return fmt.Errorf("failed to fetch payment with "+ + "complete data: %w", err) + } + + if err := mpPayment.Registrable(); err != nil { + return fmt.Errorf("htlc attempt not registrable: %w", + err) + } + + // Verify the attempt is compatible with the existing payment. + if err := verifyAttempt(mpPayment, attempt); err != nil { + return fmt.Errorf("failed to verify attempt: %w", err) + } + + // Register the plain HTLC attempt next. + sessionKey := attempt.SessionKey() + sessionKeyBytes := sessionKey.Serialize() + + _, err = db.InsertHtlcAttempt(ctx, sqlc.InsertHtlcAttemptParams{ + PaymentID: dbPayment.Payment.ID, + AttemptIndex: int64(attempt.AttemptID), + SessionKey: sessionKeyBytes, + AttemptTime: attempt.AttemptTime, + PaymentHash: paymentHash[:], + FirstHopAmountMsat: int64( + attempt.Route.FirstHopAmount.Val.Int(), + ), + RouteTotalTimeLock: int32(attempt.Route.TotalTimeLock), + RouteTotalAmount: int64(attempt.Route.TotalAmount), + RouteSourceKey: attempt.Route.SourcePubKey[:], + }) + if err != nil { + return fmt.Errorf("failed to insert HTLC "+ + "attempt: %w", err) + } + + // Insert the route level first hop custom records. + attemptFirstHopCustomRecords := attempt.Route. + FirstHopWireCustomRecords + + for key, value := range attemptFirstHopCustomRecords { + //nolint:ll + err = db.InsertPaymentAttemptFirstHopCustomRecord( + ctx, + sqlc.InsertPaymentAttemptFirstHopCustomRecordParams{ + HtlcAttemptIndex: int64(attempt.AttemptID), + Key: int64(key), + Value: value, + }, + ) + if err != nil { + return fmt.Errorf("failed to insert "+ + "payment attempt first hop custom "+ + "record: %w", err) + } + } + + // Insert the route hops. + err = s.insertRouteHops( + ctx, db, attempt.Route.Hops, attempt.AttemptID, + ) + if err != nil { + return fmt.Errorf("failed to insert route hops: %w", + err) + } + + // We fetch the HTLC attempts again to recalculate the payment + // state after the attempt is registered. This also makes sure + // we have the right data in case multiple attempts are + // registered concurrently. + // + // NOTE: While the caller is responsible for serializing calls + // to RegisterAttempt per payment hash (see PaymentControl + // interface), we still refetch here to guarantee we return + // consistent, up-to-date data that reflects all changes made + // within this transaction. + mpPayment, err = fetchPaymentWithCompleteData( + ctx, s.cfg.QueryCfg, db, dbPayment, + ) + if err != nil { + return fmt.Errorf("failed to fetch payment with "+ + "complete data: %w", err) + } + + return nil + }, func() { + mpPayment = nil + }) + if err != nil { + return nil, fmt.Errorf("failed to register attempt: %w", err) + } + + return mpPayment, nil +} + +// SettleAttempt marks the specified HTLC attempt as successfully settled, +// recording the payment preimage and settlement time. The preimage serves as +// cryptographic proof of payment and is atomically saved to the database. +// +// This method is part of the PaymentControl interface, which is embedded in +// the PaymentWriter interface and ultimately the DB interface. It represents +// step 3a in the payment lifecycle control flow (step 3b is FailAttempt), +// called after RegisterAttempt when an HTLC successfully completes. +func (s *SQLStore) SettleAttempt(ctx context.Context, paymentHash lntypes.Hash, + attemptID uint64, settleInfo *HTLCSettleInfo) (*MPPayment, error) { + + var mpPayment *MPPayment + + err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error { + dbPayment, err := fetchPaymentByHash(ctx, db, paymentHash) + if err != nil { + return err + } + + paymentStatus, err := computePaymentStatusFromDB( + ctx, s.cfg.QueryCfg, db, dbPayment, + ) + if err != nil { + return fmt.Errorf("failed to compute payment "+ + "status: %w", err) + } + + if err := paymentStatus.updatable(); err != nil { + return fmt.Errorf("payment is not updatable: %w", err) + } + + err = db.SettleAttempt(ctx, sqlc.SettleAttemptParams{ + AttemptIndex: int64(attemptID), + ResolutionTime: time.Now(), + ResolutionType: int32(HTLCAttemptResolutionSettled), + SettlePreimage: settleInfo.Preimage[:], + }) + if err != nil { + return fmt.Errorf("failed to settle attempt: %w", err) + } + + // Fetch the complete payment after we settled the attempt. + mpPayment, err = fetchPaymentWithCompleteData( + ctx, s.cfg.QueryCfg, db, dbPayment, + ) + if err != nil { + return fmt.Errorf("failed to fetch payment with "+ + "complete data: %w", err) + } + + return nil + }, func() { + mpPayment = nil + }) + if err != nil { + return nil, fmt.Errorf("failed to settle attempt: %w", err) + } + + return mpPayment, nil +} + +// FailAttempt marks the specified HTLC attempt as failed, recording the +// failure reason, failure time, optional failure message, and the index of the +// node in the route that generated the failure. This information is atomically +// saved to the database for debugging and route optimization purposes. +// +// For single-path payments, failing the only attempt may lead to the payment +// being retried or ultimately failed via the Fail method. For multi-shard +// (MPP/AMP) payments, individual shard failures don't necessarily fail the +// entire payment; additional attempts can be registered until sufficient shards +// succeed or the payment is permanently failed. +// +// Returns the updated MPPayment with the attempt marked as failed and the +// payment state recalculated. The payment status remains StatusInFlight if +// other attempts are still in flight, or may transition based on the overall +// payment state. +// +// This method is part of the PaymentControl interface, which is embedded in +// the PaymentWriter interface and ultimately the DB interface. It represents +// step 3b in the payment lifecycle control flow (step 3a is SettleAttempt), +// called after RegisterAttempt when an HTLC fails. +func (s *SQLStore) FailAttempt(ctx context.Context, paymentHash lntypes.Hash, + attemptID uint64, failInfo *HTLCFailInfo) (*MPPayment, error) { + + var mpPayment *MPPayment + + err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error { + // Make sure the payment exists. + dbPayment, err := fetchPaymentByHash(ctx, db, paymentHash) + if err != nil { + return err + } + + paymentStatus, err := computePaymentStatusFromDB( + ctx, s.cfg.QueryCfg, db, dbPayment, + ) + if err != nil { + return fmt.Errorf("failed to compute payment "+ + "status: %w", err) + } + + // We check if the payment is updatable before failing the + // attempt. + if err := paymentStatus.updatable(); err != nil { + return fmt.Errorf("payment is not updatable: %w", err) + } + + var failureMsg bytes.Buffer + if failInfo.Message != nil { + err := lnwire.EncodeFailureMessage( + &failureMsg, failInfo.Message, 0, + ) + if err != nil { + return fmt.Errorf("failed to encode "+ + "failure message: %w", err) + } + } + + err = db.FailAttempt(ctx, sqlc.FailAttemptParams{ + AttemptIndex: int64(attemptID), + ResolutionTime: time.Now(), + ResolutionType: int32(HTLCAttemptResolutionFailed), + FailureSourceIndex: sqldb.SQLInt32( + failInfo.FailureSourceIndex, + ), + HtlcFailReason: sqldb.SQLInt32(failInfo.Reason), + FailureMsg: failureMsg.Bytes(), + }) + if err != nil { + return fmt.Errorf("failed to fail attempt: %w", err) + } + + mpPayment, err = fetchPaymentWithCompleteData( + ctx, s.cfg.QueryCfg, db, dbPayment, + ) + if err != nil { + return fmt.Errorf("failed to fetch payment with "+ + "complete data: %w", err) + } + + return nil + }, func() { + mpPayment = nil + }) + if err != nil { + return nil, fmt.Errorf("failed to fail attempt: %w", err) + } + + return mpPayment, nil +} + +// Fail records the ultimate reason why a payment failed. This method stores +// the failure reason for record keeping but does not enforce that all HTLC +// attempts are resolved - HTLCs may still be in flight when this is called. +// +// The payment's actual status transition to StatusFailed is determined by the +// payment state calculation, which considers both the recorded failure reason +// and the current state of all HTLC attempts. The status will transition to +// StatusFailed once all HTLCs are resolved and/or a failure reason is recorded. +// +// NOTE: According to the interface contract, this should only be called when +// all active attempts are already failed. However, the implementation allows +// concurrent calls and does not validate this precondition, enabling the last +// failing attempt to record the failure reason without synchronization. +// +// This method is part of the PaymentControl interface, which is embedded in +// the PaymentWriter interface and ultimately the DB interface. It represents +// step 4 in the payment lifecycle control flow. +func (s *SQLStore) Fail(ctx context.Context, paymentHash lntypes.Hash, + reason FailureReason) (*MPPayment, error) { + + var mpPayment *MPPayment + + err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error { + result, err := db.FailPayment(ctx, sqlc.FailPaymentParams{ + PaymentIdentifier: paymentHash[:], + FailReason: sqldb.SQLInt32(reason), + }) + if err != nil { + return err + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return err + } + if rowsAffected == 0 { + return ErrPaymentNotInitiated + } + + payment, err := db.FetchPayment(ctx, paymentHash[:]) + if err != nil { + return fmt.Errorf("failed to fetch payment: %w", err) + } + mpPayment, err = fetchPaymentWithCompleteData( + ctx, s.cfg.QueryCfg, db, payment, + ) + if err != nil { + return fmt.Errorf("failed to fetch payment with "+ + "complete data: %w", err) + } + + return nil + }, func() { + mpPayment = nil + }) + if err != nil { + return nil, fmt.Errorf("failed to fail payment: %w", err) + } + + return mpPayment, nil +} + +// DeletePayments performs a batch deletion of payments or their failed HTLC +// attempts from the database based on the specified flags. This is a bulk +// operation that iterates through all payments and selectively deletes based +// on the criteria. +// The behavior is controlled by two flags: +// +// If failedAttemptsOnly is true, only failed HTLC attempts are deleted while +// preserving the payment records and any successful or in-flight attempts. +// The return value is always 0 when deleting attempts only. +// +// If failedAttemptsOnly is false, entire payment records are deleted including +// all associated data (HTLCs, metadata, intents). The return value is the +// number of payments deleted. +// +// The failedOnly flag further filters which payments are processed: +// - failedOnly=true, failedAttemptsOnly=true: Delete failed attempts for +// StatusFailed payments only +// - failedOnly=false, failedAttemptsOnly=true: Delete failed attempts for +// all removable payments +// - failedOnly=true, failedAttemptsOnly=false: Delete entire payment records +// for StatusFailed payments only +// - failedOnly=false, failedAttemptsOnly=false: Delete all removable payment +// records (StatusInitiated, StatusSucceeded, StatusFailed) +// +// Safety checks applied to all operations: +// - Payments with StatusInFlight are always skipped (cannot be safely deleted +// while HTLCs are on the network) +// - The payment status must pass the removable() check +// +// Returns the number of complete payments deleted (0 if only deleting failed +// attempts). This is useful for cleanup operations, administrative maintenance, +// or freeing up database storage. +// +// This method is part of the PaymentWriter interface, which is embedded in +// the DB interface. +// +// TODO(ziggie): batch and use iterator instead, moreover we dont need to fetch +// the complete payment data for each payment, we can just fetch the payment ID +// and the resolution types to decide if the payment is removable. +func (s *SQLStore) DeletePayments(ctx context.Context, failedOnly, + failedHtlcsOnly bool) (int, error) { + + var numPayments int + + extractCursor := func(row sqlc.FilterPaymentsRow) int64 { + return row.Payment.ID + } + + err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error { + // collectFunc extracts the payment ID from each payment row. + collectFunc := func(row sqlc.FilterPaymentsRow) (int64, error) { + return row.Payment.ID, nil + } + + // batchDataFunc loads only HTLC resolution types for a batch + // of payments, which is sufficient to determine payment status. + batchDataFunc := func(ctx context.Context, paymentIDs []int64) ( + *paymentStatusData, error) { + + return batchLoadPaymentResolutions( + ctx, s.cfg.QueryCfg, db, paymentIDs, + ) + } + + // processPayment processes each payment with the lightweight + // batch-loaded resolution data. + processPayment := func(ctx context.Context, + dbPayment sqlc.FilterPaymentsRow, + batchData *paymentStatusData) error { + + payment := dbPayment.Payment + + // Compute the payment status from resolution types and + // failure reason without building the complete payment. + resolutionTypes := batchData.resolutionTypes[payment.ID] + status, err := computePaymentStatusFromResolutions( + resolutionTypes, payment.FailReason, + ) + if err != nil { + return fmt.Errorf("failed to compute payment "+ + "status: %w", err) + } + + // Payments which are not final yet cannot be deleted. + // we skip them. + if err := status.removable(); err != nil { + return nil + } + + // If we are only deleting failed payments, we skip + // if the payment is not failed. + if failedOnly && status != StatusFailed { + return nil + } + + // If we are only deleting failed HTLCs, we delete them + // and return early. + if failedHtlcsOnly { + return db.DeleteFailedAttempts( + ctx, payment.ID, + ) + } + + // Otherwise we delete the payment. + err = db.DeletePayment(ctx, payment.ID) + if err != nil { + return fmt.Errorf("failed to delete "+ + "payment: %w", err) + } + + numPayments++ + + return nil + } + + queryFunc := func(ctx context.Context, lastID int64, + limit int32) ([]sqlc.FilterPaymentsRow, error) { + + filterParams := sqlc.FilterPaymentsParams{ + NumLimit: limit, + IndexOffsetGet: sqldb.SQLInt64( + lastID, + ), + } + + return db.FilterPayments(ctx, filterParams) + } + + return sqldb.ExecuteCollectAndBatchWithSharedDataQuery( + ctx, s.cfg.QueryCfg, int64(-1), queryFunc, + extractCursor, collectFunc, batchDataFunc, + processPayment, + ) + }, func() { + numPayments = 0 + }) + if err != nil { + return 0, fmt.Errorf("failed to delete payments "+ + "(failedOnly: %v, failedHtlcsOnly: %v): %w", + failedOnly, failedHtlcsOnly, err) + } + + return numPayments, nil +} diff --git a/payments/db/sql_store_test.go b/payments/db/sql_store_test.go new file mode 100644 index 00000000000..0f4f310f38e --- /dev/null +++ b/payments/db/sql_store_test.go @@ -0,0 +1,229 @@ +//go:build test_db_sqlite || test_db_postgres + +package paymentsdb + +import ( + "database/sql" + "testing" + + "github.com/stretchr/testify/require" +) + +// TestComputePaymentStatus tests the SQL to domain type conversion logic in +// computePaymentStatusFromResolutions. This is a pure unit test with no +// database interaction. However the function is only used in the SQL store and +// used sql data types so we test it in a sql specific file. +func TestComputePaymentStatus(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + resolutionTypes []sql.NullInt32 + failReason sql.NullInt32 + expectedStatus PaymentStatus + expectError bool + }{ + { + name: "all NULL resolutions means in-flight", + resolutionTypes: []sql.NullInt32{ + {Valid: false}, // NULL = in-flight + {Valid: false}, + }, + failReason: sql.NullInt32{Valid: false}, + expectedStatus: StatusInFlight, + }, + { + name: "settled resolution without fail reason", + resolutionTypes: []sql.NullInt32{{ + Int32: int32(HTLCAttemptResolutionSettled), + Valid: true, + }}, + failReason: sql.NullInt32{Valid: false}, + expectedStatus: StatusSucceeded, + }, + { + name: "failed resolution without fail reason", + resolutionTypes: []sql.NullInt32{{ + Int32: int32(HTLCAttemptResolutionFailed), + Valid: true, + }}, + failReason: sql.NullInt32{Valid: false}, + expectedStatus: StatusInFlight, + }, + { + name: "failed resolution with fail reason", + resolutionTypes: []sql.NullInt32{{ + Int32: int32(HTLCAttemptResolutionFailed), + Valid: true, + }}, + failReason: sql.NullInt32{ + Int32: int32(FailureReasonNoRoute), + Valid: true, + }, + expectedStatus: StatusFailed, + }, + { + name: "mixed: in-flight and settled", + resolutionTypes: []sql.NullInt32{ + {Valid: false}, // in-flight + { + Int32: int32( + HTLCAttemptResolutionSettled, + ), + Valid: true, + }, + }, + failReason: sql.NullInt32{Valid: false}, + expectedStatus: StatusInFlight, + }, + { + name: "mixed: in-flight and failed", + resolutionTypes: []sql.NullInt32{ + {Valid: false}, // in-flight + { + Int32: int32( + HTLCAttemptResolutionFailed, + ), + Valid: true, + }, + }, + failReason: sql.NullInt32{Valid: false}, + expectedStatus: StatusInFlight, + }, + { + name: "mixed: settled and failed", + resolutionTypes: []sql.NullInt32{ + { + Int32: int32( + HTLCAttemptResolutionSettled, + ), + Valid: true, + }, + { + Int32: int32( + HTLCAttemptResolutionFailed, + ), + Valid: true, + }, + }, + failReason: sql.NullInt32{Valid: false}, + expectedStatus: StatusSucceeded, + }, + { + name: "no resolutions, no fail reason, " + + "means initiated", + resolutionTypes: []sql.NullInt32{}, + failReason: sql.NullInt32{Valid: false}, + expectedStatus: StatusInitiated, + }, + { + name: "no resolutions with fail reason, " + + "means failed", + resolutionTypes: []sql.NullInt32{}, + failReason: sql.NullInt32{ + Int32: int32(FailureReasonNoRoute), + Valid: true, + }, + expectedStatus: StatusFailed, + }, + { + name: "unknown resolution type returns error", + resolutionTypes: []sql.NullInt32{ + {Int32: 999, Valid: true}, // invalid type + }, + failReason: sql.NullInt32{Valid: false}, + expectError: true, + }, + { + name: "all three states: in-flight, settled, failed", + resolutionTypes: []sql.NullInt32{ + { + Valid: false, // in-flight + }, + { + Int32: int32( + HTLCAttemptResolutionSettled, + ), + Valid: true, + }, + { + Int32: int32( + HTLCAttemptResolutionFailed, + ), + Valid: true, + }, + }, + failReason: sql.NullInt32{ + Int32: int32(FailureReasonTimeout), + Valid: true, + }, + expectedStatus: StatusInFlight, + }, + { + name: "multiple settled HTLCs", + resolutionTypes: []sql.NullInt32{ + { + Int32: int32( + HTLCAttemptResolutionSettled, + ), + Valid: true, + }, + { + Int32: int32( + HTLCAttemptResolutionSettled, + ), + Valid: true, + }, + { + Int32: int32( + HTLCAttemptResolutionSettled, + ), + Valid: true, + }, + }, + failReason: sql.NullInt32{Valid: false}, + expectedStatus: StatusSucceeded, + }, + { + name: "multiple failed HTLCs with fail reason", + resolutionTypes: []sql.NullInt32{ + { + Int32: int32( + HTLCAttemptResolutionFailed, + ), + Valid: true, + }, + { + Int32: int32( + HTLCAttemptResolutionFailed, + ), + Valid: true, + }, + }, + failReason: sql.NullInt32{ + Int32: int32(FailureReasonNoRoute), + Valid: true, + }, + expectedStatus: StatusFailed, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + status, err := computePaymentStatusFromResolutions( + tc.resolutionTypes, tc.failReason, + ) + + if tc.expectError { + require.Error(t, err) + return + } + + require.NoError(t, err) + require.Equal(t, tc.expectedStatus, status, + "got %s, want %s", status, tc.expectedStatus) + }) + } +} diff --git a/payments/db/test_harness.go b/payments/db/test_harness.go new file mode 100644 index 00000000000..11f88c3f833 --- /dev/null +++ b/payments/db/test_harness.go @@ -0,0 +1,26 @@ +package paymentsdb + +import ( + "testing" + + "github.com/lightningnetwork/lnd/lntypes" +) + +// TestHarness provides implementation-specific test utilities for the payments +// database. Different database backends (KV, SQL) have different internal +// structures and indexing mechanisms, so this interface allows tests to verify +// implementation-specific behavior without coupling the test logic to a +// particular backend. +type TestHarness interface { + // AssertPaymentIndex checks that a payment is correctly indexed. + // For KV: verifies the payment index bucket entry exists and points + // to the correct payment hash. + // For SQL: no-op (SQL doesn't use a separate index bucket). + AssertPaymentIndex(t *testing.T, expectedHash lntypes.Hash) + + // AssertNoIndex checks that an index for a sequence number doesn't + // exist. + // For KV: verifies the index bucket entry is deleted. + // For SQL: no-op. + AssertNoIndex(t *testing.T, seqNr uint64) +} diff --git a/payments/db/test_kvdb.go b/payments/db/test_kvdb.go index e0ee1738d7a..c2de0b43f04 100644 --- a/payments/db/test_kvdb.go +++ b/payments/db/test_kvdb.go @@ -1,14 +1,20 @@ +//go:build !test_db_sqlite && !test_db_postgres + package paymentsdb import ( + "bytes" "testing" + "github.com/btcsuite/btcwallet/walletdb" "github.com/lightningnetwork/lnd/kvdb" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // NewTestDB is a helper function that creates an BBolt database for testing. -func NewTestDB(t *testing.T, opts ...OptionModifier) DB { +func NewTestDB(t *testing.T, opts ...OptionModifier) (DB, TestHarness) { backend, backendCleanup, err := kvdb.GetTestBackend( t.TempDir(), "paymentsDB", ) @@ -19,7 +25,7 @@ func NewTestDB(t *testing.T, opts ...OptionModifier) DB { paymentDB, err := NewKVStore(backend, opts...) require.NoError(t, err) - return paymentDB + return paymentDB, &kvTestHarness{db: paymentDB} } // NewKVTestDB is a helper function that creates an BBolt database for testing @@ -38,3 +44,70 @@ func NewKVTestDB(t *testing.T, opts ...OptionModifier) *KVStore { return paymentDB } + +// kvTestHarness is the KV-specific test harness implementation. +type kvTestHarness struct { + db *KVStore +} + +// AssertPaymentIndex looks up the index for a payment in the db and checks +// that its payment hash matches the expected hash passed in. +func (h *kvTestHarness) AssertPaymentIndex(t *testing.T, + expectedHash lntypes.Hash) { + + t.Helper() + + ctx := t.Context() + + // Lookup the payment so that we have its sequence number and check + // that it has correctly been indexed in the payment indexes bucket. + pmt, err := h.db.FetchPayment(ctx, expectedHash) + require.NoError(t, err) + + hash, err := h.fetchPaymentIndexEntry(t, pmt.SequenceNum) + require.NoError(t, err) + assert.Equal(t, expectedHash, *hash) +} + +// AssertNoIndex checks that an index for the sequence number provided does not +// exist. +func (h *kvTestHarness) AssertNoIndex(t *testing.T, seqNr uint64) { + t.Helper() + + _, err := h.fetchPaymentIndexEntry(t, seqNr) + require.Equal(t, ErrNoSequenceNrIndex, err) +} + +// fetchPaymentIndexEntry gets the payment hash for the sequence number +// provided from the payment indexes bucket. +func (h *kvTestHarness) fetchPaymentIndexEntry(t *testing.T, + sequenceNumber uint64) (*lntypes.Hash, error) { + + t.Helper() + + var hash lntypes.Hash + + if err := kvdb.View(h.db.db, func(tx walletdb.ReadTx) error { + indexBucket := tx.ReadBucket(paymentsIndexBucket) + key := make([]byte, 8) + byteOrder.PutUint64(key, sequenceNumber) + + indexValue := indexBucket.Get(key) + if indexValue == nil { + return ErrNoSequenceNrIndex + } + + r := bytes.NewReader(indexValue) + + var err error + hash, err = deserializePaymentIndex(r) + + return err + }, func() { + hash = lntypes.Hash{} + }); err != nil { + return nil, err + } + + return &hash, nil +} diff --git a/payments/db/test_postgres.go b/payments/db/test_postgres.go new file mode 100644 index 00000000000..bd22703f1ff --- /dev/null +++ b/payments/db/test_postgres.go @@ -0,0 +1,95 @@ +//go:build test_db_postgres && !test_db_sqlite + +package paymentsdb + +import ( + "database/sql" + "testing" + + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/sqldb" + "github.com/stretchr/testify/require" +) + +// NewTestDB is a helper function that creates a SQLStore backed by a SQL +// database for testing. +func NewTestDB(t testing.TB, opts ...OptionModifier) (DB, TestHarness) { + db := NewTestDBWithFixture(t, nil, opts...) + return db, &noopTestHarness{} +} + +// NewTestDBFixture creates a new sqldb.TestPgFixture for testing purposes. +func NewTestDBFixture(t *testing.T) *sqldb.TestPgFixture { + pgFixture := sqldb.NewTestPgFixture( + t, sqldb.DefaultPostgresFixtureLifetime, + ) + t.Cleanup(func() { + pgFixture.TearDown(t) + }) + return pgFixture +} + +// NewTestDBWithFixture is a helper function that creates a SQLStore backed by a +// SQL database for testing. +func NewTestDBWithFixture(t testing.TB, + pgFixture *sqldb.TestPgFixture, opts ...OptionModifier) DB { + + var querier BatchedSQLQueries + if pgFixture == nil { + querier = newBatchQuerier(t) + } else { + querier = newBatchQuerierWithFixture(t, pgFixture) + } + + store, err := NewSQLStore( + &SQLStoreConfig{ + QueryCfg: sqldb.DefaultPostgresConfig(), + }, querier, opts..., + ) + require.NoError(t, err) + + return store +} + +// newBatchQuerier creates a new BatchedSQLQueries instance for testing +// using a PostgreSQL database fixture. +func newBatchQuerier(t testing.TB) BatchedSQLQueries { + pgFixture := sqldb.NewTestPgFixture( + t, sqldb.DefaultPostgresFixtureLifetime, + ) + t.Cleanup(func() { + pgFixture.TearDown(t) + }) + + return newBatchQuerierWithFixture(t, pgFixture) +} + +// newBatchQuerierWithFixture creates a new BatchedSQLQueries instance for +// testing using a PostgreSQL database fixture. +func newBatchQuerierWithFixture(t testing.TB, + pgFixture *sqldb.TestPgFixture) BatchedSQLQueries { + + db := sqldb.NewTestPostgresDB(t, pgFixture).BaseDB + + return sqldb.NewTransactionExecutor( + db, func(tx *sql.Tx) SQLQueries { + return db.WithTx(tx) + }, + ) +} + +// noopTestHarness is the SQL test harness implementation. Since SQL doesn't +// use a separate payment index bucket like KV, these assertions are no-ops. +type noopTestHarness struct{} + +// AssertPaymentIndex is a no-op for SQL implementations. +func (h *noopTestHarness) AssertPaymentIndex(t *testing.T, + expectedHash lntypes.Hash) { + + // No-op: SQL doesn't use a separate index bucket. +} + +// AssertNoIndex is a no-op for SQL implementations. +func (h *noopTestHarness) AssertNoIndex(t *testing.T, seqNr uint64) { + // No-op: SQL doesn't use a separate index bucket. +} diff --git a/payments/db/test_sqlite.go b/payments/db/test_sqlite.go new file mode 100644 index 00000000000..99d10478051 --- /dev/null +++ b/payments/db/test_sqlite.go @@ -0,0 +1,74 @@ +//go:build !test_db_postgres && test_db_sqlite + +package paymentsdb + +import ( + "database/sql" + "testing" + + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/sqldb" + "github.com/stretchr/testify/require" +) + +// NewTestDB is a helper function that creates a SQLStore backed by a SQL +// database for testing. +func NewTestDB(t testing.TB, opts ...OptionModifier) (DB, TestHarness) { + db := NewTestDBWithFixture(t, nil, opts...) + return db, &noopTestHarness{} +} + +// NewTestDBFixture is a no-op for the sqlite build. +func NewTestDBFixture(_ *testing.T) *sqldb.TestPgFixture { + return nil +} + +// NewTestDBWithFixture is a helper function that creates a SQLStore backed by a +// SQL database for testing. +func NewTestDBWithFixture(t testing.TB, _ *sqldb.TestPgFixture, + opts ...OptionModifier) DB { + + store, err := NewSQLStore( + &SQLStoreConfig{ + QueryCfg: sqldb.DefaultSQLiteConfig(), + }, newBatchQuerier(t), opts..., + ) + require.NoError(t, err) + return store +} + +// newBatchQuerier creates a new BatchedSQLQueries instance for testing +// using a SQLite database. +func newBatchQuerier(t testing.TB) BatchedSQLQueries { + return newBatchQuerierWithFixture(t, nil) +} + +// newBatchQuerierWithFixture creates a new BatchedSQLQueries instance for +// testing using a SQLite database. +func newBatchQuerierWithFixture(t testing.TB, + _ *sqldb.TestPgFixture) BatchedSQLQueries { + + db := sqldb.NewTestSqliteDB(t).BaseDB + + return sqldb.NewTransactionExecutor( + db, func(tx *sql.Tx) SQLQueries { + return db.WithTx(tx) + }, + ) +} + +// noopTestHarness is the SQL test harness implementation. Since SQL doesn't +// use a separate payment index bucket like KV, these assertions are no-ops. +type noopTestHarness struct{} + +// AssertPaymentIndex is a no-op for SQL implementations. +func (h *noopTestHarness) AssertPaymentIndex(t *testing.T, + expectedHash lntypes.Hash) { + + // No-op: SQL doesn't use a separate index bucket. +} + +// AssertNoIndex is a no-op for SQL implementations. +func (h *noopTestHarness) AssertNoIndex(t *testing.T, seqNr uint64) { + // No-op: SQL doesn't use a separate index bucket. +} diff --git a/routing/control_tower.go b/routing/control_tower.go index 2b9e7dd9d28..1c246f17d9b 100644 --- a/routing/control_tower.go +++ b/routing/control_tower.go @@ -1,6 +1,7 @@ package routing import ( + "context" "sync" "github.com/lightningnetwork/lnd/lntypes" @@ -19,17 +20,19 @@ type ControlTower interface { // also notifies subscribers of the payment creation. // // NOTE: Subscribers should be notified by the new state of the payment. - InitPayment(lntypes.Hash, *paymentsdb.PaymentCreationInfo) error + InitPayment(context.Context, lntypes.Hash, + *paymentsdb.PaymentCreationInfo) error // DeleteFailedAttempts removes all failed HTLCs from the db. It should // be called for a given payment whenever all inflight htlcs are // completed, and the payment has reached a final settled state. - DeleteFailedAttempts(lntypes.Hash) error + DeleteFailedAttempts(context.Context, lntypes.Hash) error // RegisterAttempt atomically records the provided HTLCAttemptInfo. // // NOTE: Subscribers should be notified by the new state of the payment. - RegisterAttempt(lntypes.Hash, *paymentsdb.HTLCAttemptInfo) error + RegisterAttempt(context.Context, lntypes.Hash, + *paymentsdb.HTLCAttemptInfo) error // SettleAttempt marks the given attempt settled with the preimage. If // this is a multi shard payment, this might implicitly mean the the @@ -41,18 +44,19 @@ type ControlTower interface { // for record keeping. // // NOTE: Subscribers should be notified by the new state of the payment. - SettleAttempt(lntypes.Hash, uint64, *paymentsdb.HTLCSettleInfo) ( - *paymentsdb.HTLCAttempt, error) + SettleAttempt(context.Context, lntypes.Hash, uint64, + *paymentsdb.HTLCSettleInfo) (*paymentsdb.HTLCAttempt, error) // FailAttempt marks the given payment attempt failed. // // NOTE: Subscribers should be notified by the new state of the payment. - FailAttempt(lntypes.Hash, uint64, *paymentsdb.HTLCFailInfo) ( - *paymentsdb.HTLCAttempt, error) + FailAttempt(context.Context, lntypes.Hash, uint64, + *paymentsdb.HTLCFailInfo) (*paymentsdb.HTLCAttempt, error) // FetchPayment fetches the payment corresponding to the given payment // hash. - FetchPayment(paymentHash lntypes.Hash) (paymentsdb.DBMPPayment, error) + FetchPayment(ctx context.Context, + paymentHash lntypes.Hash) (paymentsdb.DBMPPayment, error) // FailPayment transitions a payment into the Failed state, and records // the ultimate reason the payment failed. Note that this should only @@ -62,10 +66,12 @@ type ControlTower interface { // payment. // // NOTE: Subscribers should be notified by the new state of the payment. - FailPayment(lntypes.Hash, paymentsdb.FailureReason) error + FailPayment(context.Context, lntypes.Hash, + paymentsdb.FailureReason) error // FetchInFlightPayments returns all payments with status InFlight. - FetchInFlightPayments() ([]*paymentsdb.MPPayment, error) + FetchInFlightPayments(ctx context.Context) ([]*paymentsdb.MPPayment, + error) // SubscribePayment subscribes to updates for the payment with the given // hash. A first update with the current state of the payment is always @@ -161,10 +167,10 @@ func NewControlTower(db paymentsdb.DB) ControlTower { // making sure it does not already exist as an in-flight payment. Then this // method returns successfully, the payment is guaranteed to be in the // Initiated state. -func (p *controlTower) InitPayment(paymentHash lntypes.Hash, - info *paymentsdb.PaymentCreationInfo) error { +func (p *controlTower) InitPayment(ctx context.Context, + paymentHash lntypes.Hash, info *paymentsdb.PaymentCreationInfo) error { - err := p.db.InitPayment(paymentHash, info) + err := p.db.InitPayment(ctx, paymentHash, info) if err != nil { return err } @@ -174,7 +180,7 @@ func (p *controlTower) InitPayment(paymentHash lntypes.Hash, p.paymentsMtx.Lock(paymentHash) defer p.paymentsMtx.Unlock(paymentHash) - payment, err := p.db.FetchPayment(paymentHash) + payment, err := p.db.FetchPayment(ctx, paymentHash) if err != nil { return err } @@ -186,19 +192,21 @@ func (p *controlTower) InitPayment(paymentHash lntypes.Hash, // DeleteFailedAttempts deletes all failed htlcs if the payment was // successfully settled. -func (p *controlTower) DeleteFailedAttempts(paymentHash lntypes.Hash) error { - return p.db.DeleteFailedAttempts(paymentHash) +func (p *controlTower) DeleteFailedAttempts(ctx context.Context, + paymentHash lntypes.Hash) error { + + return p.db.DeleteFailedAttempts(ctx, paymentHash) } // RegisterAttempt atomically records the provided HTLCAttemptInfo to the // DB. -func (p *controlTower) RegisterAttempt(paymentHash lntypes.Hash, - attempt *paymentsdb.HTLCAttemptInfo) error { +func (p *controlTower) RegisterAttempt(ctx context.Context, + paymentHash lntypes.Hash, attempt *paymentsdb.HTLCAttemptInfo) error { p.paymentsMtx.Lock(paymentHash) defer p.paymentsMtx.Unlock(paymentHash) - payment, err := p.db.RegisterAttempt(paymentHash, attempt) + payment, err := p.db.RegisterAttempt(ctx, paymentHash, attempt) if err != nil { return err } @@ -212,14 +220,17 @@ func (p *controlTower) RegisterAttempt(paymentHash lntypes.Hash, // SettleAttempt marks the given attempt settled with the preimage. If // this is a multi shard payment, this might implicitly mean the the // full payment succeeded. -func (p *controlTower) SettleAttempt(paymentHash lntypes.Hash, - attemptID uint64, settleInfo *paymentsdb.HTLCSettleInfo) ( - *paymentsdb.HTLCAttempt, error) { +func (p *controlTower) SettleAttempt(ctx context.Context, + paymentHash lntypes.Hash, attemptID uint64, + settleInfo *paymentsdb.HTLCSettleInfo) (*paymentsdb.HTLCAttempt, + error) { p.paymentsMtx.Lock(paymentHash) defer p.paymentsMtx.Unlock(paymentHash) - payment, err := p.db.SettleAttempt(paymentHash, attemptID, settleInfo) + payment, err := p.db.SettleAttempt( + ctx, paymentHash, attemptID, settleInfo, + ) if err != nil { return nil, err } @@ -231,14 +242,14 @@ func (p *controlTower) SettleAttempt(paymentHash lntypes.Hash, } // FailAttempt marks the given payment attempt failed. -func (p *controlTower) FailAttempt(paymentHash lntypes.Hash, - attemptID uint64, failInfo *paymentsdb.HTLCFailInfo) ( - *paymentsdb.HTLCAttempt, error) { +func (p *controlTower) FailAttempt(ctx context.Context, + paymentHash lntypes.Hash, attemptID uint64, + failInfo *paymentsdb.HTLCFailInfo) (*paymentsdb.HTLCAttempt, error) { p.paymentsMtx.Lock(paymentHash) defer p.paymentsMtx.Unlock(paymentHash) - payment, err := p.db.FailAttempt(paymentHash, attemptID, failInfo) + payment, err := p.db.FailAttempt(ctx, paymentHash, attemptID, failInfo) if err != nil { return nil, err } @@ -250,10 +261,11 @@ func (p *controlTower) FailAttempt(paymentHash lntypes.Hash, } // FetchPayment fetches the payment corresponding to the given payment hash. -func (p *controlTower) FetchPayment(paymentHash lntypes.Hash) ( +func (p *controlTower) FetchPayment(ctx context.Context, + paymentHash lntypes.Hash) ( paymentsdb.DBMPPayment, error) { - return p.db.FetchPayment(paymentHash) + return p.db.FetchPayment(ctx, paymentHash) } // FailPayment transitions a payment into the Failed state, and records the @@ -263,13 +275,13 @@ func (p *controlTower) FetchPayment(paymentHash lntypes.Hash) ( // // NOTE: This method will overwrite the failure reason if the payment is already // failed. -func (p *controlTower) FailPayment(paymentHash lntypes.Hash, - reason paymentsdb.FailureReason) error { +func (p *controlTower) FailPayment(ctx context.Context, + paymentHash lntypes.Hash, reason paymentsdb.FailureReason) error { p.paymentsMtx.Lock(paymentHash) defer p.paymentsMtx.Unlock(paymentHash) - payment, err := p.db.Fail(paymentHash, reason) + payment, err := p.db.Fail(ctx, paymentHash, reason) if err != nil { return err } @@ -281,10 +293,10 @@ func (p *controlTower) FailPayment(paymentHash lntypes.Hash, } // FetchInFlightPayments returns all payments with status InFlight. -func (p *controlTower) FetchInFlightPayments() ([]*paymentsdb.MPPayment, - error) { +func (p *controlTower) FetchInFlightPayments( + ctx context.Context) ([]*paymentsdb.MPPayment, error) { - return p.db.FetchInFlightPayments() + return p.db.FetchInFlightPayments(ctx) } // SubscribePayment subscribes to updates for the payment with the given hash. A @@ -293,12 +305,14 @@ func (p *controlTower) FetchInFlightPayments() ([]*paymentsdb.MPPayment, func (p *controlTower) SubscribePayment(paymentHash lntypes.Hash) ( ControlTowerSubscriber, error) { + ctx := context.TODO() + // Take lock before querying the db to prevent missing or duplicating an // update. p.paymentsMtx.Lock(paymentHash) defer p.paymentsMtx.Unlock(paymentHash) - payment, err := p.db.FetchPayment(paymentHash) + payment, err := p.db.FetchPayment(ctx, paymentHash) if err != nil { return nil, err } @@ -335,6 +349,8 @@ func (p *controlTower) SubscribePayment(paymentHash lntypes.Hash) ( func (p *controlTower) SubscribeAllPayments() (ControlTowerSubscriber, error) { subscriber := newControlTowerSubscriber() + ctx := context.TODO() + // Add the subscriber to the list before fetching in-flight payments, so // no events are missed. If a payment attempt update occurs after // appending and before fetching in-flight payments, an out-of-order @@ -346,7 +362,7 @@ func (p *controlTower) SubscribeAllPayments() (ControlTowerSubscriber, error) { p.subscribersMtx.Unlock() log.Debugf("Scanning for inflight payments") - inflightPayments, err := p.db.FetchInFlightPayments() + inflightPayments, err := p.db.FetchInFlightPayments(ctx) if err != nil { return nil, err } diff --git a/routing/control_tower_test.go b/routing/control_tower_test.go index de0aacf880b..697770ff506 100644 --- a/routing/control_tower_test.go +++ b/routing/control_tower_test.go @@ -50,10 +50,7 @@ func TestControlTowerSubscribeUnknown(t *testing.T) { db := initDB(t) - paymentDB, err := paymentsdb.NewKVStore( - db, - paymentsdb.WithKeepFailedPaymentAttempts(true), - ) + paymentDB, err := paymentsdb.NewKVStore(db) require.NoError(t, err) pControl := NewControlTower(paymentDB) @@ -81,7 +78,7 @@ func TestControlTowerSubscribeSuccess(t *testing.T) { t.Fatal(err) } - err = pControl.InitPayment(info.PaymentIdentifier, info) + err = pControl.InitPayment(t.Context(), info.PaymentIdentifier, info) if err != nil { t.Fatal(err) } @@ -92,7 +89,9 @@ func TestControlTowerSubscribeSuccess(t *testing.T) { require.NoError(t, err, "expected subscribe to succeed, but got") // Register an attempt. - err = pControl.RegisterAttempt(info.PaymentIdentifier, attempt) + err = pControl.RegisterAttempt( + t.Context(), info.PaymentIdentifier, attempt, + ) if err != nil { t.Fatal(err) } @@ -106,7 +105,8 @@ func TestControlTowerSubscribeSuccess(t *testing.T) { Preimage: preimg, } htlcAttempt, err := pControl.SettleAttempt( - info.PaymentIdentifier, attempt.AttemptID, &settleInfo, + t.Context(), info.PaymentIdentifier, attempt.AttemptID, + &settleInfo, ) if err != nil { t.Fatal(err) @@ -179,17 +179,11 @@ func TestControlTowerSubscribeSuccess(t *testing.T) { func TestKVStoreSubscribeFail(t *testing.T) { t.Parallel() - t.Run("register attempt, keep failed payments", func(t *testing.T) { - testKVStoreSubscribeFail(t, true, true) + t.Run("register attempt", func(t *testing.T) { + testKVStoreSubscribeFail(t, true) }) - t.Run("register attempt, delete failed payments", func(t *testing.T) { - testKVStoreSubscribeFail(t, true, false) - }) - t.Run("no register attempt, keep failed payments", func(t *testing.T) { - testKVStoreSubscribeFail(t, false, true) - }) - t.Run("no register attempt, delete failed payments", func(t *testing.T) { - testKVStoreSubscribeFail(t, false, false) + t.Run("no register attempt", func(t *testing.T) { + testKVStoreSubscribeFail(t, false) }) } @@ -200,10 +194,7 @@ func TestKVStoreSubscribeAllSuccess(t *testing.T) { db := initDB(t) - paymentDB, err := paymentsdb.NewKVStore( - db, - paymentsdb.WithKeepFailedPaymentAttempts(true), - ) + paymentDB, err := paymentsdb.NewKVStore(db) require.NoError(t, err) pControl := NewControlTower(paymentDB) @@ -212,7 +203,7 @@ func TestKVStoreSubscribeAllSuccess(t *testing.T) { info1, attempt1, preimg1, err := genInfo() require.NoError(t, err) - err = pControl.InitPayment(info1.PaymentIdentifier, info1) + err = pControl.InitPayment(t.Context(), info1.PaymentIdentifier, info1) require.NoError(t, err) // Subscription should succeed and immediately report the Initiated @@ -221,18 +212,22 @@ func TestKVStoreSubscribeAllSuccess(t *testing.T) { require.NoError(t, err, "expected subscribe to succeed, but got: %v") // Register an attempt. - err = pControl.RegisterAttempt(info1.PaymentIdentifier, attempt1) + err = pControl.RegisterAttempt( + t.Context(), info1.PaymentIdentifier, attempt1, + ) require.NoError(t, err) // Initiate a second payment after the subscription is already active. info2, attempt2, preimg2, err := genInfo() require.NoError(t, err) - err = pControl.InitPayment(info2.PaymentIdentifier, info2) + err = pControl.InitPayment(t.Context(), info2.PaymentIdentifier, info2) require.NoError(t, err) // Register an attempt on the second payment. - err = pControl.RegisterAttempt(info2.PaymentIdentifier, attempt2) + err = pControl.RegisterAttempt( + t.Context(), info2.PaymentIdentifier, attempt2, + ) require.NoError(t, err) // Mark the first payment as successful. @@ -240,7 +235,8 @@ func TestKVStoreSubscribeAllSuccess(t *testing.T) { Preimage: preimg1, } htlcAttempt1, err := pControl.SettleAttempt( - info1.PaymentIdentifier, attempt1.AttemptID, &settleInfo1, + t.Context(), info1.PaymentIdentifier, attempt1.AttemptID, + &settleInfo1, ) require.NoError(t, err) require.Equal( @@ -253,7 +249,8 @@ func TestKVStoreSubscribeAllSuccess(t *testing.T) { Preimage: preimg2, } htlcAttempt2, err := pControl.SettleAttempt( - info2.PaymentIdentifier, attempt2.AttemptID, &settleInfo2, + t.Context(), info2.PaymentIdentifier, attempt2.AttemptID, + &settleInfo2, ) require.NoError(t, err) require.Equal( @@ -325,10 +322,7 @@ func TestKVStoreSubscribeAllImmediate(t *testing.T) { db := initDB(t) - paymentDB, err := paymentsdb.NewKVStore( - db, - paymentsdb.WithKeepFailedPaymentAttempts(true), - ) + paymentDB, err := paymentsdb.NewKVStore(db) require.NoError(t, err) pControl := NewControlTower(paymentDB) @@ -337,11 +331,13 @@ func TestKVStoreSubscribeAllImmediate(t *testing.T) { info, attempt, _, err := genInfo() require.NoError(t, err) - err = pControl.InitPayment(info.PaymentIdentifier, info) + err = pControl.InitPayment(t.Context(), info.PaymentIdentifier, info) require.NoError(t, err) // Register a payment update. - err = pControl.RegisterAttempt(info.PaymentIdentifier, attempt) + err = pControl.RegisterAttempt( + t.Context(), info.PaymentIdentifier, attempt, + ) require.NoError(t, err) subscription, err := pControl.SubscribeAllPayments() @@ -374,10 +370,7 @@ func TestKVStoreUnsubscribeSuccess(t *testing.T) { db := initDB(t) - paymentDB, err := paymentsdb.NewKVStore( - db, - paymentsdb.WithKeepFailedPaymentAttempts(true), - ) + paymentDB, err := paymentsdb.NewKVStore(db) require.NoError(t, err) pControl := NewControlTower(paymentDB) @@ -392,7 +385,7 @@ func TestKVStoreUnsubscribeSuccess(t *testing.T) { info, attempt, _, err := genInfo() require.NoError(t, err) - err = pControl.InitPayment(info.PaymentIdentifier, info) + err = pControl.InitPayment(t.Context(), info.PaymentIdentifier, info) require.NoError(t, err) // Assert all subscriptions receive the update. @@ -414,7 +407,9 @@ func TestKVStoreUnsubscribeSuccess(t *testing.T) { subscription1.Close() // Register a payment update. - err = pControl.RegisterAttempt(info.PaymentIdentifier, attempt) + err = pControl.RegisterAttempt( + t.Context(), info.PaymentIdentifier, attempt, + ) require.NoError(t, err) // Assert only subscription 2 receives the update. @@ -435,7 +430,8 @@ func TestKVStoreUnsubscribeSuccess(t *testing.T) { Reason: paymentsdb.HTLCFailInternal, } _, err = pControl.FailAttempt( - info.PaymentIdentifier, attempt.AttemptID, &failInfo, + t.Context(), info.PaymentIdentifier, attempt.AttemptID, + &failInfo, ) require.NoError(t, err, "unable to fail htlc") @@ -444,17 +440,10 @@ func TestKVStoreUnsubscribeSuccess(t *testing.T) { require.Len(t, subscription2.Updates(), 0) } -func testKVStoreSubscribeFail(t *testing.T, registerAttempt, - keepFailedPaymentAttempts bool) { - +func testKVStoreSubscribeFail(t *testing.T, registerAttempt bool) { db := initDB(t) - paymentDB, err := paymentsdb.NewKVStore( - db, - paymentsdb.WithKeepFailedPaymentAttempts( - keepFailedPaymentAttempts, - ), - ) + paymentDB, err := paymentsdb.NewKVStore(db) require.NoError(t, err) pControl := NewControlTower(paymentDB) @@ -465,7 +454,7 @@ func testKVStoreSubscribeFail(t *testing.T, registerAttempt, t.Fatal(err) } - err = pControl.InitPayment(info.PaymentIdentifier, info) + err = pControl.InitPayment(t.Context(), info.PaymentIdentifier, info) if err != nil { t.Fatal(err) } @@ -479,17 +468,18 @@ func testKVStoreSubscribeFail(t *testing.T, registerAttempt, // making any attempts at all. if registerAttempt { // Register an attempt. - err = pControl.RegisterAttempt(info.PaymentIdentifier, attempt) - if err != nil { - t.Fatal(err) - } + err = pControl.RegisterAttempt( + t.Context(), info.PaymentIdentifier, attempt, + ) + require.NoError(t, err) // Fail the payment attempt. failInfo := paymentsdb.HTLCFailInfo{ Reason: paymentsdb.HTLCFailInternal, } htlcAttempt, err := pControl.FailAttempt( - info.PaymentIdentifier, attempt.AttemptID, &failInfo, + t.Context(), info.PaymentIdentifier, attempt.AttemptID, + &failInfo, ) if err != nil { t.Fatalf("unable to fail htlc: %v", err) @@ -501,7 +491,8 @@ func testKVStoreSubscribeFail(t *testing.T, registerAttempt, // Mark the payment as failed. err = pControl.FailPayment( - info.PaymentIdentifier, paymentsdb.FailureReasonTimeout, + t.Context(), info.PaymentIdentifier, + paymentsdb.FailureReasonTimeout, ) if err != nil { t.Fatal(err) diff --git a/routing/mock_test.go b/routing/mock_test.go index 19a76ee9010..472f1261623 100644 --- a/routing/mock_test.go +++ b/routing/mock_test.go @@ -1,6 +1,7 @@ package routing import ( + "context" "errors" "fmt" "sync" @@ -296,8 +297,8 @@ func makeMockControlTower() *mockControlTowerOld { } } -func (m *mockControlTowerOld) InitPayment(phash lntypes.Hash, - c *paymentsdb.PaymentCreationInfo) error { +func (m *mockControlTowerOld) InitPayment(_ context.Context, + phash lntypes.Hash, c *paymentsdb.PaymentCreationInfo) error { if m.init != nil { m.init <- initArgs{c} @@ -327,7 +328,9 @@ func (m *mockControlTowerOld) InitPayment(phash lntypes.Hash, return nil } -func (m *mockControlTowerOld) DeleteFailedAttempts(phash lntypes.Hash) error { +func (m *mockControlTowerOld) DeleteFailedAttempts(_ context.Context, + phash lntypes.Hash) error { + p, ok := m.payments[phash] if !ok { return paymentsdb.ErrPaymentNotInitiated @@ -353,8 +356,8 @@ func (m *mockControlTowerOld) DeleteFailedAttempts(phash lntypes.Hash) error { return nil } -func (m *mockControlTowerOld) RegisterAttempt(phash lntypes.Hash, - a *paymentsdb.HTLCAttemptInfo) error { +func (m *mockControlTowerOld) RegisterAttempt(_ context.Context, + phash lntypes.Hash, a *paymentsdb.HTLCAttemptInfo) error { if m.registerAttempt != nil { m.registerAttempt <- registerAttemptArgs{a} @@ -407,8 +410,8 @@ func (m *mockControlTowerOld) RegisterAttempt(phash lntypes.Hash, return nil } -func (m *mockControlTowerOld) SettleAttempt(phash lntypes.Hash, - pid uint64, settleInfo *paymentsdb.HTLCSettleInfo) ( +func (m *mockControlTowerOld) SettleAttempt(_ context.Context, + phash lntypes.Hash, pid uint64, settleInfo *paymentsdb.HTLCSettleInfo) ( *paymentsdb.HTLCAttempt, error) { if m.settleAttempt != nil { @@ -450,8 +453,9 @@ func (m *mockControlTowerOld) SettleAttempt(phash lntypes.Hash, return nil, fmt.Errorf("pid not found") } -func (m *mockControlTowerOld) FailAttempt(phash lntypes.Hash, pid uint64, - failInfo *paymentsdb.HTLCFailInfo) (*paymentsdb.HTLCAttempt, error) { +func (m *mockControlTowerOld) FailAttempt(_ context.Context, phash lntypes.Hash, + pid uint64, failInfo *paymentsdb.HTLCFailInfo) (*paymentsdb.HTLCAttempt, + error) { if m.failAttempt != nil { m.failAttempt <- failAttemptArgs{failInfo} @@ -489,7 +493,7 @@ func (m *mockControlTowerOld) FailAttempt(phash lntypes.Hash, pid uint64, return nil, fmt.Errorf("pid not found") } -func (m *mockControlTowerOld) FailPayment(phash lntypes.Hash, +func (m *mockControlTowerOld) FailPayment(_ context.Context, phash lntypes.Hash, reason paymentsdb.FailureReason) error { m.Lock() @@ -509,8 +513,8 @@ func (m *mockControlTowerOld) FailPayment(phash lntypes.Hash, return nil } -func (m *mockControlTowerOld) FetchPayment(phash lntypes.Hash) ( - paymentsdb.DBMPPayment, error) { +func (m *mockControlTowerOld) FetchPayment(_ context.Context, + phash lntypes.Hash) (paymentsdb.DBMPPayment, error) { m.Lock() defer m.Unlock() @@ -545,7 +549,7 @@ func (m *mockControlTowerOld) fetchPayment(phash lntypes.Hash) ( return mp, nil } -func (m *mockControlTowerOld) FetchInFlightPayments() ( +func (m *mockControlTowerOld) FetchInFlightPayments(_ context.Context) ( []*paymentsdb.MPPayment, error) { if m.fetchInFlight != nil { @@ -733,26 +737,28 @@ type mockControlTower struct { var _ ControlTower = (*mockControlTower)(nil) -func (m *mockControlTower) InitPayment(phash lntypes.Hash, +func (m *mockControlTower) InitPayment(_ context.Context, phash lntypes.Hash, c *paymentsdb.PaymentCreationInfo) error { args := m.Called(phash, c) return args.Error(0) } -func (m *mockControlTower) DeleteFailedAttempts(phash lntypes.Hash) error { +func (m *mockControlTower) DeleteFailedAttempts(_ context.Context, + phash lntypes.Hash) error { + args := m.Called(phash) return args.Error(0) } -func (m *mockControlTower) RegisterAttempt(phash lntypes.Hash, - a *paymentsdb.HTLCAttemptInfo) error { +func (m *mockControlTower) RegisterAttempt(_ context.Context, + phash lntypes.Hash, a *paymentsdb.HTLCAttemptInfo) error { args := m.Called(phash, a) return args.Error(0) } -func (m *mockControlTower) SettleAttempt(phash lntypes.Hash, +func (m *mockControlTower) SettleAttempt(_ context.Context, phash lntypes.Hash, pid uint64, settleInfo *paymentsdb.HTLCSettleInfo) ( *paymentsdb.HTLCAttempt, error) { @@ -766,8 +772,9 @@ func (m *mockControlTower) SettleAttempt(phash lntypes.Hash, return attempt.(*paymentsdb.HTLCAttempt), args.Error(1) } -func (m *mockControlTower) FailAttempt(phash lntypes.Hash, pid uint64, - failInfo *paymentsdb.HTLCFailInfo) (*paymentsdb.HTLCAttempt, error) { +func (m *mockControlTower) FailAttempt(_ context.Context, phash lntypes.Hash, + pid uint64, failInfo *paymentsdb.HTLCFailInfo) (*paymentsdb.HTLCAttempt, + error) { args := m.Called(phash, pid, failInfo) @@ -779,15 +786,15 @@ func (m *mockControlTower) FailAttempt(phash lntypes.Hash, pid uint64, return attempt.(*paymentsdb.HTLCAttempt), args.Error(1) } -func (m *mockControlTower) FailPayment(phash lntypes.Hash, +func (m *mockControlTower) FailPayment(_ context.Context, phash lntypes.Hash, reason paymentsdb.FailureReason) error { args := m.Called(phash, reason) return args.Error(0) } -func (m *mockControlTower) FetchPayment(phash lntypes.Hash) ( - paymentsdb.DBMPPayment, error) { +func (m *mockControlTower) FetchPayment(_ context.Context, + phash lntypes.Hash) (paymentsdb.DBMPPayment, error) { args := m.Called(phash) @@ -800,7 +807,7 @@ func (m *mockControlTower) FetchPayment(phash lntypes.Hash) ( return payment, args.Error(1) } -func (m *mockControlTower) FetchInFlightPayments() ( +func (m *mockControlTower) FetchInFlightPayments(_ context.Context) ( []*paymentsdb.MPPayment, error) { args := m.Called() diff --git a/routing/payment_lifecycle.go b/routing/payment_lifecycle.go index 8353cba157f..488df5b796e 100644 --- a/routing/payment_lifecycle.go +++ b/routing/payment_lifecycle.go @@ -128,7 +128,7 @@ const ( // results is sent back. then process its result here. When there's no need to // wait for results, the method will exit with `stepExit` such that the payment // lifecycle loop will terminate. -func (p *paymentLifecycle) decideNextStep( +func (p *paymentLifecycle) decideNextStep(ctx context.Context, payment paymentsdb.DBMPPayment) (stateStep, error) { // Check whether we could make new HTLC attempts. @@ -168,7 +168,7 @@ func (p *paymentLifecycle) decideNextStep( // stepSkip and move to the next lifecycle iteration, which will // refresh the payment and wait for the next attempt result, if // any. - _, err := p.handleAttemptResult(r.attempt, r.result) + _, err := p.handleAttemptResult(ctx, r.attempt, r.result) // We would only get a DB-related error here, which will cause // us to abort the payment flow. @@ -190,6 +190,17 @@ func (p *paymentLifecycle) decideNextStep( func (p *paymentLifecycle) resumePayment(ctx context.Context) ([32]byte, *route.Route, error) { + // We need to make sure we can still do db operations after the context + // is cancelled. + // + // TODO(ziggie): This is a workaround to avoid a greater refactor of the + // payment lifecycle. We can currently not rely on the parent context + // because this method is also collecting the results of inflight HTLCs + // after the context is cancelled. So we need to make sure we only use + // the current context to stop creating new attempts but use this + // cleanupCtx to do all the db operations. + cleanupCtx := context.WithoutCancel(ctx) + // When the payment lifecycle loop exits, we make sure to signal any // sub goroutine of the HTLC attempt to exit, then wait for them to // return. @@ -198,7 +209,7 @@ func (p *paymentLifecycle) resumePayment(ctx context.Context) ([32]byte, // If we had any existing attempts outstanding, we'll start by spinning // up goroutines that'll collect their results and deliver them to the // lifecycle loop below. - payment, err := p.reloadInflightAttempts() + payment, err := p.reloadInflightAttempts(ctx) if err != nil { return [32]byte{}, nil, err } @@ -239,7 +250,7 @@ lifecycle: } // We update the payment state on every iteration. - currentPayment, ps, err := p.reloadPayment() + currentPayment, ps, err := p.reloadPayment(cleanupCtx) if err != nil { return exitWithErr(err) } @@ -260,7 +271,7 @@ lifecycle: // // Now decide the next step of the current lifecycle. - step, err := p.decideNextStep(payment) + step, err := p.decideNextStep(cleanupCtx, payment) if err != nil { return exitWithErr(err) } @@ -284,7 +295,7 @@ lifecycle: } // Now request a route to be used to create our HTLC attempt. - rt, err := p.requestRoute(ps) + rt, err := p.requestRoute(cleanupCtx, ps) if err != nil { return exitWithErr(err) } @@ -303,13 +314,15 @@ lifecycle: log.Tracef("Found route: %s", lnutils.SpewLogClosure(rt.Hops)) // We found a route to try, create a new HTLC attempt to try. - attempt, err := p.registerAttempt(rt, ps.RemainingAmt) + attempt, err := p.registerAttempt( + cleanupCtx, rt, ps.RemainingAmt, + ) if err != nil { return exitWithErr(err) } // Once the attempt is created, send it to the htlcswitch. - result, err := p.sendAttempt(attempt) + result, err := p.sendAttempt(cleanupCtx, attempt) if err != nil { return exitWithErr(err) } @@ -325,13 +338,16 @@ lifecycle: // terminal condition. We either return the settled preimage or the // payment's failure reason. // - // Optionally delete the failed attempts from the database. Depends on - // the database options deleting attempts is not allowed so this will - // just be a no-op. - err = p.router.cfg.Control.DeleteFailedAttempts(p.identifier) - if err != nil { - log.Errorf("Error deleting failed htlc attempts for payment "+ - "%v: %v", p.identifier, err) + // Optionally delete the failed attempts from the database. If we are + // configured to keep failed payment attempts, we skip deletion. + if !p.router.cfg.KeepFailedPaymentAttempts { + err = p.router.cfg.Control.DeleteFailedAttempts( + cleanupCtx, p.identifier, + ) + if err != nil { + log.Errorf("Error deleting failed htlc attempts "+ + "for payment %v: %v", p.identifier, err) + } } htlc, failure := payment.TerminalInfo() @@ -364,11 +380,18 @@ func (p *paymentLifecycle) checkContext(ctx context.Context) error { p.identifier.String()) } + // The context is already cancelled at this point, so we create + // a new context so the payment can successfully be marked as + // failed. + cleanupCtx := context.WithoutCancel(ctx) + // By marking the payment failed, depending on whether it has // inflight HTLCs or not, its status will now either be // `StatusInflight` or `StatusFailed`. In either case, no more // HTLCs will be attempted. - err := p.router.cfg.Control.FailPayment(p.identifier, reason) + err := p.router.cfg.Control.FailPayment( + cleanupCtx, p.identifier, reason, + ) if err != nil { return fmt.Errorf("FailPayment got %w", err) } @@ -386,7 +409,7 @@ func (p *paymentLifecycle) checkContext(ctx context.Context) error { // requestRoute is responsible for finding a route to be used to create an HTLC // attempt. -func (p *paymentLifecycle) requestRoute( +func (p *paymentLifecycle) requestRoute(ctx context.Context, ps *paymentsdb.MPPaymentState) (*route.Route, error) { remainingFees := p.calcFeeBudget(ps.FeesPaid) @@ -430,7 +453,9 @@ func (p *paymentLifecycle) requestRoute( log.Warnf("Marking payment %v permanently failed with no route: %v", p.identifier, failureCode) - err = p.router.cfg.Control.FailPayment(p.identifier, failureCode) + err = p.router.cfg.Control.FailPayment( + ctx, p.identifier, failureCode, + ) if err != nil { return nil, fmt.Errorf("FailPayment got: %w", err) } @@ -581,7 +606,7 @@ func (p *paymentLifecycle) collectResult( // registerAttempt is responsible for creating and saving an HTLC attempt in db // by using the route info provided. The `remainingAmt` is used to decide // whether this is the last attempt. -func (p *paymentLifecycle) registerAttempt(rt *route.Route, +func (p *paymentLifecycle) registerAttempt(ctx context.Context, rt *route.Route, remainingAmt lnwire.MilliSatoshi) (*paymentsdb.HTLCAttempt, error) { // If this route will consume the last remaining amount to send @@ -601,7 +626,7 @@ func (p *paymentLifecycle) registerAttempt(rt *route.Route, // Switch for its whereabouts. The route is needed to handle the result // when it eventually comes back. err = p.router.cfg.Control.RegisterAttempt( - p.identifier, &attempt.HTLCAttemptInfo, + ctx, p.identifier, &attempt.HTLCAttemptInfo, ) return attempt, err @@ -657,7 +682,7 @@ func (p *paymentLifecycle) createNewPaymentAttempt(rt *route.Route, // sendAttempt attempts to send the current attempt to the switch to complete // the payment. If this attempt fails, then we'll continue on to the next // available route. -func (p *paymentLifecycle) sendAttempt( +func (p *paymentLifecycle) sendAttempt(ctx context.Context, attempt *paymentsdb.HTLCAttempt) (*attemptResult, error) { log.Debugf("Sending HTLC attempt(id=%v, total_amt=%v, first_hop_amt=%d"+ @@ -689,7 +714,7 @@ func (p *paymentLifecycle) sendAttempt( "payment=%v, err:%v", attempt.AttemptID, p.identifier, err) - return p.failAttempt(attempt.AttemptID, err) + return p.failAttempt(ctx, attempt.AttemptID, err) } htlcAdd.OnionBlob = onionBlob @@ -703,7 +728,7 @@ func (p *paymentLifecycle) sendAttempt( log.Errorf("Failed sending attempt %d for payment %v to "+ "switch: %v", attempt.AttemptID, p.identifier, err) - return p.handleSwitchErr(attempt, err) + return p.handleSwitchErr(ctx, attempt, err) } log.Debugf("Attempt %v for payment %v successfully sent to switch, "+ @@ -794,7 +819,7 @@ func (p *paymentLifecycle) amendFirstHopData(rt *route.Route) error { // failAttemptAndPayment fails both the payment and its attempt via the // router's control tower, which marks the payment as failed in db. -func (p *paymentLifecycle) failPaymentAndAttempt( +func (p *paymentLifecycle) failPaymentAndAttempt(ctx context.Context, attemptID uint64, reason *paymentsdb.FailureReason, sendErr error) (*attemptResult, error) { @@ -806,14 +831,16 @@ func (p *paymentLifecycle) failPaymentAndAttempt( // NOTE: we must fail the payment first before failing the attempt. // Otherwise, once the attempt is marked as failed, another goroutine // might make another attempt while we are failing the payment. - err := p.router.cfg.Control.FailPayment(p.identifier, *reason) + err := p.router.cfg.Control.FailPayment( + ctx, p.identifier, *reason, + ) if err != nil { log.Errorf("Unable to fail payment: %v", err) return nil, err } // Fail the attempt. - return p.failAttempt(attemptID, sendErr) + return p.failAttempt(ctx, attemptID, sendErr) } // handleSwitchErr inspects the given error from the Switch and determines @@ -824,7 +851,8 @@ func (p *paymentLifecycle) failPaymentAndAttempt( // the error type, the error is either the final outcome of the payment or we // need to continue with an alternative route. A final outcome is indicated by // a non-nil reason value. -func (p *paymentLifecycle) handleSwitchErr(attempt *paymentsdb.HTLCAttempt, +func (p *paymentLifecycle) handleSwitchErr(ctx context.Context, + attempt *paymentsdb.HTLCAttempt, sendErr error) (*attemptResult, error) { internalErrorReason := paymentsdb.FailureReasonError @@ -851,11 +879,11 @@ func (p *paymentLifecycle) handleSwitchErr(attempt *paymentsdb.HTLCAttempt, // Fail the attempt only if there's no reason. if reason == nil { // Fail the attempt. - return p.failAttempt(attemptID, sendErr) + return p.failAttempt(ctx, attemptID, sendErr) } // Otherwise fail both the payment and the attempt. - return p.failPaymentAndAttempt(attemptID, reason, sendErr) + return p.failPaymentAndAttempt(ctx, attemptID, reason, sendErr) } // If this attempt ID is unknown to the Switch, it means it was never @@ -866,7 +894,7 @@ func (p *paymentLifecycle) handleSwitchErr(attempt *paymentsdb.HTLCAttempt, log.Warnf("Failing attempt=%v for payment=%v as it's not "+ "found in the Switch", attempt.AttemptID, p.identifier) - return p.failAttempt(attemptID, sendErr) + return p.failAttempt(ctx, attemptID, sendErr) } if errors.Is(sendErr, htlcswitch.ErrUnreadableFailureMessage) { @@ -888,7 +916,7 @@ func (p *paymentLifecycle) handleSwitchErr(attempt *paymentsdb.HTLCAttempt, ok := errors.As(sendErr, &rtErr) if !ok { return p.failPaymentAndAttempt( - attemptID, &internalErrorReason, sendErr, + ctx, attemptID, &internalErrorReason, sendErr, ) } @@ -914,7 +942,7 @@ func (p *paymentLifecycle) handleSwitchErr(attempt *paymentsdb.HTLCAttempt, ) if err != nil { return p.failPaymentAndAttempt( - attemptID, &internalErrorReason, sendErr, + ctx, attemptID, &internalErrorReason, sendErr, ) } @@ -998,7 +1026,7 @@ func (p *paymentLifecycle) handleFailureMessage(rt *route.Route, } // failAttempt calls control tower to fail the current payment attempt. -func (p *paymentLifecycle) failAttempt(attemptID uint64, +func (p *paymentLifecycle) failAttempt(ctx context.Context, attemptID uint64, sendError error) (*attemptResult, error) { log.Warnf("Attempt %v for payment %v failed: %v", attemptID, @@ -1017,7 +1045,7 @@ func (p *paymentLifecycle) failAttempt(attemptID uint64, } attempt, err := p.router.cfg.Control.FailAttempt( - p.identifier, attemptID, failInfo, + ctx, p.identifier, attemptID, failInfo, ) if err != nil { return nil, err @@ -1111,10 +1139,10 @@ func (p *paymentLifecycle) patchLegacyPaymentHash( // reloadInflightAttempts is called when the payment lifecycle is resumed after // a restart. It reloads all inflight attempts from the control tower and // collects the results of the attempts that have been sent before. -func (p *paymentLifecycle) reloadInflightAttempts() (paymentsdb.DBMPPayment, - error) { +func (p *paymentLifecycle) reloadInflightAttempts( + ctx context.Context) (paymentsdb.DBMPPayment, error) { - payment, err := p.router.cfg.Control.FetchPayment(p.identifier) + payment, err := p.router.cfg.Control.FetchPayment(ctx, p.identifier) if err != nil { return nil, err } @@ -1136,11 +1164,12 @@ func (p *paymentLifecycle) reloadInflightAttempts() (paymentsdb.DBMPPayment, } // reloadPayment returns the latest payment found in the db (control tower). -func (p *paymentLifecycle) reloadPayment() (paymentsdb.DBMPPayment, +func (p *paymentLifecycle) reloadPayment( + ctx context.Context) (paymentsdb.DBMPPayment, *paymentsdb.MPPaymentState, error) { // Read the db to get the latest state of the payment. - payment, err := p.router.cfg.Control.FetchPayment(p.identifier) + payment, err := p.router.cfg.Control.FetchPayment(ctx, p.identifier) if err != nil { return nil, nil, err } @@ -1157,13 +1186,14 @@ func (p *paymentLifecycle) reloadPayment() (paymentsdb.DBMPPayment, // handleAttemptResult processes the result of an HTLC attempt returned from // the htlcswitch. -func (p *paymentLifecycle) handleAttemptResult(attempt *paymentsdb.HTLCAttempt, +func (p *paymentLifecycle) handleAttemptResult(ctx context.Context, + attempt *paymentsdb.HTLCAttempt, result *htlcswitch.PaymentResult) (*attemptResult, error) { // If the result has an error, we need to further process it by failing // the attempt and maybe fail the payment. if result.Error != nil { - return p.handleSwitchErr(attempt, result.Error) + return p.handleSwitchErr(ctx, attempt, result.Error) } // We got an attempt settled result back from the switch. @@ -1181,7 +1211,7 @@ func (p *paymentLifecycle) handleAttemptResult(attempt *paymentsdb.HTLCAttempt, // In case of success we atomically store settle result to the DB and // move the shard to the settled state. htlcAttempt, err := p.router.cfg.Control.SettleAttempt( - p.identifier, attempt.AttemptID, + ctx, p.identifier, attempt.AttemptID, &paymentsdb.HTLCSettleInfo{ Preimage: result.Preimage, SettleTime: p.router.cfg.Clock.Now(), @@ -1206,7 +1236,7 @@ func (p *paymentLifecycle) handleAttemptResult(attempt *paymentsdb.HTLCAttempt, // available from the Switch, then records the attempt outcome with the control // tower. An attemptResult is returned, indicating the final outcome of this // HTLC attempt. -func (p *paymentLifecycle) collectAndHandleResult( +func (p *paymentLifecycle) collectAndHandleResult(ctx context.Context, attempt *paymentsdb.HTLCAttempt) (*attemptResult, error) { result, err := p.collectResult(attempt) @@ -1214,5 +1244,5 @@ func (p *paymentLifecycle) collectAndHandleResult( return nil, err } - return p.handleAttemptResult(attempt, result) + return p.handleAttemptResult(ctx, attempt, result) } diff --git a/routing/payment_lifecycle_test.go b/routing/payment_lifecycle_test.go index 7e94315a7dc..564942d327e 100644 --- a/routing/payment_lifecycle_test.go +++ b/routing/payment_lifecycle_test.go @@ -393,7 +393,7 @@ func TestRequestRouteSucceed(t *testing.T) { mock.Anything, ).Return(dummyRoute, nil) - result, err := p.requestRoute(ps) + result, err := p.requestRoute(t.Context(), ps) require.NoError(t, err, "expect no error") require.Equal(t, dummyRoute, result, "returned route not matched") @@ -430,7 +430,7 @@ func TestRequestRouteHandleCriticalErr(t *testing.T) { mock.Anything, ).Return(nil, errDummy) - result, err := p.requestRoute(ps) + result, err := p.requestRoute(t.Context(), ps) // Expect an error is returned since it's critical. require.ErrorIs(t, err, errDummy, "error not matched") @@ -470,7 +470,7 @@ func TestRequestRouteHandleNoRouteErr(t *testing.T) { p.identifier, paymentsdb.FailureReasonNoRoute, ).Return(nil).Once() - result, err := p.requestRoute(ps) + result, err := p.requestRoute(t.Context(), ps) // Expect no error is returned since it's not critical. require.NoError(t, err, "expected no error") @@ -513,7 +513,7 @@ func TestRequestRouteFailPaymentError(t *testing.T) { mock.Anything, ).Return(nil, errNoTlvPayload) - result, err := p.requestRoute(ps) + result, err := p.requestRoute(t.Context(), ps) // Expect an error is returned. require.ErrorIs(t, err, errDummy, "error not matched") @@ -599,7 +599,7 @@ func TestDecideNextStep(t *testing.T) { // Once the setup is finished, run the test cases. t.Run(tc.name, func(t *testing.T) { - step, err := p.decideNextStep(payment) + step, err := p.decideNextStep(t.Context(), payment) require.Equal(t, tc.expectedStep, step) require.ErrorIs(t, tc.expectedErr, err) }) @@ -628,7 +628,7 @@ func TestDecideNextStepOnRouterQuit(t *testing.T) { close(p.router.quit) // Call the method under test. - step, err := p.decideNextStep(payment) + step, err := p.decideNextStep(t.Context(), payment) // We expect stepExit and an error to be returned. require.Equal(t, stepExit, step) @@ -657,7 +657,7 @@ func TestDecideNextStepOnLifecycleQuit(t *testing.T) { close(p.quit) // Call the method under test. - step, err := p.decideNextStep(payment) + step, err := p.decideNextStep(t.Context(), payment) // We expect stepExit and an error to be returned. require.Equal(t, stepExit, step) @@ -716,7 +716,7 @@ func TestDecideNextStepHandleAttemptResultSucceed(t *testing.T) { mock.Anything).Return(attempt, nil).Once() // Call the method under test. - step, err := p.decideNextStep(payment) + step, err := p.decideNextStep(t.Context(), payment) // We expect stepSkip and no error to be returned. require.Equal(t, stepSkip, step) @@ -774,7 +774,7 @@ func TestDecideNextStepHandleAttemptResultFail(t *testing.T) { mock.Anything).Return(attempt, errDummy).Once() // Call the method under test. - step, err := p.decideNextStep(payment) + step, err := p.decideNextStep(t.Context(), payment) // We expect stepExit and the above error to be returned. require.Equal(t, stepExit, step) @@ -1280,6 +1280,156 @@ func TestResumePaymentSuccess(t *testing.T) { require.Equal(t, 1, m.collectResultsCount) } +// TestKeepFailedPaymentAttempts tests that DeleteFailedAttempts is +// called or skipped based on the KeepFailedPaymentAttempts +// configuration of the router. +func TestKeepFailedPaymentAttempts(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + keepFailedPaymentAttempts bool + expectDeleteCalled bool + }{ + { + name: "keep failed attempts - " + + "delete not called", + keepFailedPaymentAttempts: true, + expectDeleteCalled: false, + }, + { + name: "delete failed attempts - " + + "delete called", + keepFailedPaymentAttempts: false, + expectDeleteCalled: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // Create a test paymentLifecycle with the initial two + // calls mocked. + p, m := setupTestPaymentLifecycle(t) + + // Set the KeepFailedPaymentAttempts configuration. + p.router.cfg.KeepFailedPaymentAttempts = + tc.keepFailedPaymentAttempts + + // Create a dummy route that will be returned by + // `RequestRoute`. + paymentAmt := lnwire.MilliSatoshi(10000) + rt := createDummyRoute(t, paymentAmt) + + // We now enter the payment lifecycle loop. + // + // 1.1. calls `FetchPayment` and return the payment. + m.control.On("FetchPayment", p.identifier). + Return(m.payment, nil).Once() + + // 1.2. calls `GetState` and return the state. + ps := &paymentsdb.MPPaymentState{ + RemainingAmt: paymentAmt, + } + m.payment.On("GetState").Return(ps).Once() + + // NOTE: GetStatus is only used to populate the logs + // which is not critical so we loosen the checks on how + // many times it's been called. + m.payment.On("GetStatus"). + Return(paymentsdb.StatusInFlight) + + // 1.3. decideNextStep now returns stepProceed. + m.payment.On("AllowMoreAttempts"). + Return(true, nil).Once() + + // 1.4. mock requestRoute to return an route. + m.paySession.On("RequestRoute", + paymentAmt, p.feeLimit, + uint32(ps.NumAttemptsInFlight), + uint32(p.currentHeight), mock.Anything, + ).Return(rt, nil).Once() + + // 1.5. mock `registerAttempt` to return an attempt. + // + // Mock NextPaymentID to always return the attemptID. + attemptID := uint64(1) + p.router.cfg.NextPaymentID = func() (uint64, error) { + return attemptID, nil + } + + // Mock shardTracker to return the mock shard. + m.shardTracker.On("NewShard", + attemptID, true, + ).Return(m.shard, nil).Once() + + // Mock the methods on the shard. + m.shard.On("MPP").Return(&record.MPP{}).Twice(). + On("AMP").Return(nil).Once(). + On("Hash").Return(p.identifier).Once() + + // Mock the time and expect it to be called. + m.clock.On("Now").Return(time.Now()) + + // We now register attempt and return no error. + m.control.On("RegisterAttempt", + p.identifier, mock.Anything, + ).Return(nil).Once() + + // 1.6. mock `sendAttempt` to succeed, which brings us + // into the next iteration of the lifecycle. + m.payer.On("SendHTLC", + mock.Anything, attemptID, mock.Anything, + ).Return(nil).Once() + + // We now enter the second iteration of the lifecycle + // loop. + // + // 2.1. calls `FetchPayment` and return the payment. + m.control.On("FetchPayment", p.identifier). + Return(m.payment, nil).Once() + + // 2.2. calls `GetState` and return the state. + m.payment.On("GetState").Return(ps). + Run(func(args mock.Arguments) { + ps.RemainingAmt = 0 + }).Once() + + // 2.3. decideNextStep now returns stepExit and exits + // the loop. + m.payment.On("AllowMoreAttempts"). + Return(false, nil).Once(). + On("NeedWaitAttempts").Return(false, nil).Once() + + // Conditionally expect DeleteFailedAttempts to be + // called based on the configuration. + if tc.expectDeleteCalled { + m.control.On("DeleteFailedAttempts", + p.identifier).Return(nil).Once() + } + // If expectDeleteCalled is false, we don't set up the + // expectation, which means the mock will fail if it's + // called. + + // Finally, mock the `TerminalInfo` to return the + // settled attempt. Create a SettleAttempt. + testPreimage := lntypes.Preimage{1, 2, 3} + settledAttempt := makeSettledAttempt( + t, int(paymentAmt), testPreimage, + ) + m.payment.On("TerminalInfo"). + Return(settledAttempt, nil).Once() + + // Send the payment and assert the preimage is matched. + sendPaymentAndAssertSucceeded(t, p, testPreimage) + + // Expected collectResultAsync to called. + require.Equal(t, 1, m.collectResultsCount) + }) + } +} + // TestResumePaymentSuccessWithTwoAttempts checks a successful payment flow // with two HTLC attempts. // @@ -1467,7 +1617,7 @@ func TestCollectResultExitOnErr(t *testing.T) { m.clock.On("Now").Return(time.Now()) // Now call the method under test. - result, err := p.collectAndHandleResult(attempt) + result, err := p.collectAndHandleResult(t.Context(), attempt) require.ErrorIs(t, err, errDummy, "expected dummy error") require.Nil(t, result, "expected nil attempt") } @@ -1513,7 +1663,7 @@ func TestCollectResultExitOnResultErr(t *testing.T) { m.clock.On("Now").Return(time.Now()) // Now call the method under test. - result, err := p.collectAndHandleResult(attempt) + result, err := p.collectAndHandleResult(t.Context(), attempt) require.ErrorIs(t, err, errDummy, "expected dummy error") require.Nil(t, result, "expected nil attempt") } @@ -1539,7 +1689,7 @@ func TestCollectResultExitOnSwitchQuit(t *testing.T) { }) // Now call the method under test. - result, err := p.collectAndHandleResult(attempt) + result, err := p.collectAndHandleResult(t.Context(), attempt) require.ErrorIs(t, err, htlcswitch.ErrSwitchExiting, "expected switch exit") require.Nil(t, result, "expected nil attempt") @@ -1566,7 +1716,7 @@ func TestCollectResultExitOnRouterQuit(t *testing.T) { }) // Now call the method under test. - result, err := p.collectAndHandleResult(attempt) + result, err := p.collectAndHandleResult(t.Context(), attempt) require.ErrorIs(t, err, ErrRouterShuttingDown, "expected router exit") require.Nil(t, result, "expected nil attempt") } @@ -1592,7 +1742,7 @@ func TestCollectResultExitOnLifecycleQuit(t *testing.T) { }) // Now call the method under test. - result, err := p.collectAndHandleResult(attempt) + result, err := p.collectAndHandleResult(t.Context(), attempt) require.ErrorIs(t, err, ErrPaymentLifecycleExiting, "expected lifecycle exit") require.Nil(t, result, "expected nil attempt") @@ -1636,7 +1786,7 @@ func TestCollectResultExitOnSettleErr(t *testing.T) { m.clock.On("Now").Return(time.Now()) // Now call the method under test. - result, err := p.collectAndHandleResult(attempt) + result, err := p.collectAndHandleResult(t.Context(), attempt) require.ErrorIs(t, err, errDummy, "expected settle error") require.Nil(t, result, "expected nil attempt") } @@ -1678,7 +1828,7 @@ func TestCollectResultSuccess(t *testing.T) { m.clock.On("Now").Return(time.Now()) // Now call the method under test. - result, err := p.collectAndHandleResult(attempt) + result, err := p.collectAndHandleResult(t.Context(), attempt) require.NoError(t, err, "expected no error") require.Equal(t, preimage, result.attempt.Settle.Preimage, "preimage mismatch") @@ -1762,7 +1912,9 @@ func TestHandleAttemptResultWithError(t *testing.T) { // Call the method under test and expect the dummy error to be // returned. - attemptResult, err := p.handleAttemptResult(attempt, result) + attemptResult, err := p.handleAttemptResult( + t.Context(), attempt, result, + ) require.ErrorIs(t, err, errDummy, "expected fail error") require.Nil(t, attemptResult, "expected nil attempt result") } @@ -1800,7 +1952,9 @@ func TestHandleAttemptResultSuccess(t *testing.T) { // Call the method under test and expect the dummy error to be // returned. - attemptResult, err := p.handleAttemptResult(attempt, result) + attemptResult, err := p.handleAttemptResult( + t.Context(), attempt, result, + ) require.NoError(t, err, "expected no error") require.Equal(t, attempt, attemptResult.attempt) } @@ -1846,7 +2000,7 @@ func TestReloadInflightAttemptsLegacy(t *testing.T) { }) // Now call the method under test. - payment, err := p.reloadInflightAttempts() + payment, err := p.reloadInflightAttempts(t.Context()) require.NoError(t, err) require.Equal(t, m.payment, payment) diff --git a/routing/route/route.go b/routing/route/route.go index 1bb52badbdb..a575c415bd7 100644 --- a/routing/route/route.go +++ b/routing/route/route.go @@ -164,6 +164,9 @@ type Hop struct { // The only reason we are keeping this member is that it could be the // case that we have serialised hops persisted to disk where // LegacyPayload is true. + // + // TODO(ziggie): Remove this field once we phase out the kv backend + // for payments. LegacyPayload bool // Metadata is additional data that is sent along with the payment to diff --git a/routing/router.go b/routing/router.go index 3c35b7c52cc..a58f7a9088c 100644 --- a/routing/router.go +++ b/routing/router.go @@ -295,6 +295,10 @@ type Config struct { // TrafficShaper is an optional traffic shaper that can be used to // control the outgoing channel of a payment. TrafficShaper fn.Option[htlcswitch.AuxTrafficShaper] + + // KeepFailedPaymentAttempts indicates whether to keep failed payment + // attempts in the database. + KeepFailedPaymentAttempts bool } // EdgeLocator is a struct used to identify a specific edge. @@ -896,8 +900,8 @@ func (l *LightningPayment) Identifier() [32]byte { // will be returned which describes the path the successful payment traversed // within the network to reach the destination. Additionally, the payment // preimage will also be returned. -func (r *ChannelRouter) SendPayment(payment *LightningPayment) ([32]byte, - *route.Route, error) { +func (r *ChannelRouter) SendPayment(ctx context.Context, + payment *LightningPayment) ([32]byte, *route.Route, error) { paySession, shardTracker, err := r.PreparePayment(payment) if err != nil { @@ -908,7 +912,7 @@ func (r *ChannelRouter) SendPayment(payment *LightningPayment) ([32]byte, spewPayment(payment)) return r.sendPayment( - context.Background(), payment.FeeLimit, payment.Identifier(), + ctx, payment.FeeLimit, payment.Identifier(), payment.PayAttemptTimeout, paySession, shardTracker, payment.FirstHopCustomRecords, ) @@ -967,6 +971,8 @@ func spewPayment(payment *LightningPayment) lnutils.LogClosure { func (r *ChannelRouter) PreparePayment(payment *LightningPayment) ( PaymentSession, shards.ShardTracker, error) { + ctx := context.TODO() + // Assemble any custom data we want to send to the first hop only. var firstHopData fn.Option[tlv.Blob] if len(payment.FirstHopCustomRecords) > 0 { @@ -1026,7 +1032,7 @@ func (r *ChannelRouter) PreparePayment(payment *LightningPayment) ( ) } - err = r.cfg.Control.InitPayment(payment.Identifier(), info) + err = r.cfg.Control.InitPayment(ctx, payment.Identifier(), info) if err != nil { return nil, nil, err } @@ -1036,7 +1042,8 @@ func (r *ChannelRouter) PreparePayment(payment *LightningPayment) ( // SendToRoute sends a payment using the provided route and fails the payment // when an error is returned from the attempt. -func (r *ChannelRouter) SendToRoute(htlcHash lntypes.Hash, rt *route.Route, +func (r *ChannelRouter) SendToRoute(_ context.Context, htlcHash lntypes.Hash, + rt *route.Route, firstHopCustomRecords lnwire.CustomRecords) (*paymentsdb.HTLCAttempt, error) { @@ -1045,8 +1052,8 @@ func (r *ChannelRouter) SendToRoute(htlcHash lntypes.Hash, rt *route.Route, // SendToRouteSkipTempErr sends a payment using the provided route and fails // the payment ONLY when a terminal error is returned from the attempt. -func (r *ChannelRouter) SendToRouteSkipTempErr(htlcHash lntypes.Hash, - rt *route.Route, +func (r *ChannelRouter) SendToRouteSkipTempErr(_ context.Context, + htlcHash lntypes.Hash, rt *route.Route, firstHopCustomRecords lnwire.CustomRecords) (*paymentsdb.HTLCAttempt, error) { @@ -1064,13 +1071,20 @@ func (r *ChannelRouter) sendToRoute(htlcHash lntypes.Hash, rt *route.Route, firstHopCustomRecords lnwire.CustomRecords) (*paymentsdb.HTLCAttempt, error) { + // TODO(ziggie): We cannot easily thread the context from the caller + // of this method because the payment lifecycle depends on the context + // to update the db. The Sending and Receiving of results is currently + // not cleanly separated which is the reason that we cannot easily + // cancel the context and therefore cancel the ongoing payment. + ctx := context.TODO() + // Helper function to fail a payment. It makes sure the payment is only // failed once so that the failure reason is not overwritten. failPayment := func(paymentIdentifier lntypes.Hash, reason paymentsdb.FailureReason) error { payment, fetchErr := r.cfg.Control.FetchPayment( - paymentIdentifier, + ctx, paymentIdentifier, ) if fetchErr != nil { return fetchErr @@ -1084,7 +1098,9 @@ func (r *ChannelRouter) sendToRoute(htlcHash lntypes.Hash, rt *route.Route, return nil } - return r.cfg.Control.FailPayment(paymentIdentifier, reason) + return r.cfg.Control.FailPayment( + ctx, paymentIdentifier, reason, + ) } log.Debugf("SendToRoute for payment %v with skipTempErr=%v", @@ -1129,7 +1145,7 @@ func (r *ChannelRouter) sendToRoute(htlcHash lntypes.Hash, rt *route.Route, FirstHopCustomRecords: firstHopCustomRecords, } - err := r.cfg.Control.InitPayment(paymentIdentifier, info) + err := r.cfg.Control.InitPayment(ctx, paymentIdentifier, info) switch { // If this is an MPP attempt and the hash is already registered with // the database, we can go on to launch the shard. @@ -1173,7 +1189,7 @@ func (r *ChannelRouter) sendToRoute(htlcHash lntypes.Hash, rt *route.Route, // NOTE: we use zero `remainingAmt` here to simulate the same effect of // setting the lastShard to be false, which is used by previous // implementation. - attempt, err := p.registerAttempt(rt, 0) + attempt, err := p.registerAttempt(ctx, rt, 0) if err != nil { return nil, err } @@ -1182,7 +1198,7 @@ func (r *ChannelRouter) sendToRoute(htlcHash lntypes.Hash, rt *route.Route, // the `err` returned here has already been processed by // `handleSwitchErr`, which means if there's a terminal failure, the // payment has been failed. - result, err := p.sendAttempt(attempt) + result, err := p.sendAttempt(ctx, attempt) if err != nil { return nil, err } @@ -1210,7 +1226,7 @@ func (r *ChannelRouter) sendToRoute(htlcHash lntypes.Hash, rt *route.Route, // The attempt was successfully sent, wait for the result to be // available. - result, err = p.collectAndHandleResult(attempt) + result, err = p.collectAndHandleResult(ctx, attempt) if err != nil { return nil, err } @@ -1415,9 +1431,11 @@ func (r *ChannelRouter) BuildRoute(amt fn.Option[lnwire.MilliSatoshi], // resumePayments fetches inflight payments and resumes their payment // lifecycles. func (r *ChannelRouter) resumePayments() error { + ctx := context.TODO() + // Get all payments that are inflight. log.Debugf("Scanning for inflight payments") - payments, err := r.cfg.Control.FetchInFlightPayments() + payments, err := r.cfg.Control.FetchInFlightPayments(ctx) if err != nil { return err } @@ -1425,6 +1443,11 @@ func (r *ChannelRouter) resumePayments() error { log.Debugf("Scanning finished, found %d inflight payments", len(payments)) + // TODO(ziggie): Also check for payments which have no HTLCs at all + // this can happen because we register an attempt after initializing the + // payment, so there is a small chance that we init a payment but never + // register an attempt for it. + // Before we restart existing payments and start accepting more // payments to be made, we clean the network result store of the // Switch. We do this here at startup to ensure no more payments can be @@ -1525,6 +1548,8 @@ func (r *ChannelRouter) resumePayments() error { func (r *ChannelRouter) failStaleAttempt(a paymentsdb.HTLCAttempt, payHash lntypes.Hash) { + ctx := context.TODO() + // We can only fail inflight HTLCs so we skip the settled/failed ones. if a.Failure != nil || a.Settle != nil { return @@ -1608,7 +1633,7 @@ func (r *ChannelRouter) failStaleAttempt(a paymentsdb.HTLCAttempt, Reason: paymentsdb.HTLCFailUnknown, FailTime: r.cfg.Clock.Now(), } - _, err = r.cfg.Control.FailAttempt(payHash, a.AttemptID, failInfo) + _, err = r.cfg.Control.FailAttempt(ctx, payHash, a.AttemptID, failInfo) if err != nil { log.Errorf("Fail attempt=%v got error: %v", a.AttemptID, err) } diff --git a/routing/router_test.go b/routing/router_test.go index 9f088917daf..576bb35f6b6 100644 --- a/routing/router_test.go +++ b/routing/router_test.go @@ -324,7 +324,9 @@ func TestSendPaymentRouteFailureFallback(t *testing.T) { // Send off the payment request to the router, route through pham nuwen // should've been selected as a fall back and succeeded correctly. - paymentPreImage, route, err := ctx.router.SendPayment(payment) + paymentPreImage, route, err := ctx.router.SendPayment( + t.Context(), payment, + ) require.NoErrorf(t, err, "unable to send payment: %v", payment.paymentHash) @@ -403,7 +405,9 @@ func TestSendPaymentRouteInfiniteLoopWithBadHopHint(t *testing.T) { // Send off the payment request to the router, should succeed // ignoring the bad channel id hint. - paymentPreImage, route, paymentErr := ctx.router.SendPayment(payment) + paymentPreImage, route, paymentErr := ctx.router.SendPayment( + t.Context(), payment, + ) require.NoErrorf(t, paymentErr, "unable to send payment: %v", payment.paymentHash) @@ -518,7 +522,7 @@ func TestChannelUpdateValidation(t *testing.T) { // Send off the payment request to the router. The specified route // should be attempted and the channel update should be received by // graph and ignored because it is missing a valid signature. - _, err = ctx.router.SendToRoute(payment, rt, nil) + _, err = ctx.router.SendToRoute(t.Context(), payment, rt, nil) require.Error(t, err, "expected route to fail with channel update") _, e1, e2, err = ctx.graph.FetchChannelEdgesByID( @@ -538,7 +542,7 @@ func TestChannelUpdateValidation(t *testing.T) { ctx.graphBuilder.setNextReject(false) // Retry the payment using the same route as before. - _, err = ctx.router.SendToRoute(payment, rt, nil) + _, err = ctx.router.SendToRoute(t.Context(), payment, rt, nil) require.Error(t, err, "expected route to fail with channel update") // This time a valid signature was supplied and the policy change should @@ -634,7 +638,9 @@ func TestSendPaymentErrorRepeatedFeeInsufficient(t *testing.T) { // Send off the payment request to the router, route through phamnuwen // should've been selected as a fall back and succeeded correctly. - paymentPreImage, route, err := ctx.router.SendPayment(payment) + paymentPreImage, route, err := ctx.router.SendPayment( + t.Context(), payment, + ) require.NoErrorf(t, err, "unable to send payment: %v", payment.paymentHash) @@ -741,7 +747,9 @@ func TestSendPaymentErrorFeeInsufficientPrivateEdge(t *testing.T) { // Send off the payment request to the router, route through son // goku and then across the private channel to elst. - paymentPreImage, route, err := ctx.router.SendPayment(payment) + paymentPreImage, route, err := ctx.router.SendPayment( + t.Context(), payment, + ) require.NoErrorf(t, err, "unable to send payment: %v", payment.paymentHash) @@ -867,7 +875,9 @@ func TestSendPaymentPrivateEdgeUpdateFeeExceedsLimit(t *testing.T) { // Send off the payment request to the router, route through son // goku and then across the private channel to elst. - paymentPreImage, route, err := ctx.router.SendPayment(payment) + paymentPreImage, route, err := ctx.router.SendPayment( + t.Context(), payment, + ) require.NoErrorf(t, err, "unable to send payment: %v", payment.paymentHash) @@ -990,7 +1000,9 @@ func TestSendPaymentErrorNonFinalTimeLockErrors(t *testing.T) { // Send off the payment request to the router, this payment should // succeed as we should actually go through Pham Nuwen in order to get // to Sophon, even though he has higher fees. - paymentPreImage, rt, err := ctx.router.SendPayment(payment) + paymentPreImage, rt, err := ctx.router.SendPayment( + t.Context(), payment, + ) require.NoErrorf(t, err, "unable to send payment: %v", payment.paymentHash) @@ -1016,7 +1028,9 @@ func TestSendPaymentErrorNonFinalTimeLockErrors(t *testing.T) { // w.r.t to the block height, and instead go through Pham Nuwen. We // flip a bit in the payment hash to allow resending this payment. payment.paymentHash[1] ^= 1 - paymentPreImage, rt, err = ctx.router.SendPayment(payment) + paymentPreImage, rt, err = ctx.router.SendPayment( + t.Context(), payment, + ) require.NoErrorf(t, err, "unable to send payment: %v", payment.paymentHash) @@ -1085,7 +1099,7 @@ func TestSendPaymentErrorPathPruning(t *testing.T) { // When we try to dispatch that payment, we should receive an error as // both attempts should fail and cause both routes to be pruned. - _, _, err = ctx.router.SendPayment(payment) + _, _, err = ctx.router.SendPayment(t.Context(), payment) require.Error(t, err, "payment didn't return error") // The final error returned should also indicate that the peer wasn't @@ -1093,7 +1107,9 @@ func TestSendPaymentErrorPathPruning(t *testing.T) { require.Equal(t, paymentsdb.FailureReasonNoRoute, err) // Inspect the two attempts that were made before the payment failed. - p, err := ctx.router.cfg.Control.FetchPayment(*payment.paymentHash) + p, err := ctx.router.cfg.Control.FetchPayment( + t.Context(), *payment.paymentHash, + ) require.NoError(t, err) htlcs := p.GetHTLCs() @@ -1128,7 +1144,9 @@ func TestSendPaymentErrorPathPruning(t *testing.T) { // This shouldn't return an error, as we'll make a payment attempt via // the pham nuwen channel based on the assumption that there might be an // intermittent issue with the songoku <-> sophon channel. - paymentPreImage, rt, err := ctx.router.SendPayment(payment) + paymentPreImage, rt, err := ctx.router.SendPayment( + t.Context(), payment, + ) require.NoErrorf(t, err, "unable to send payment: %v", payment.paymentHash) @@ -1168,7 +1186,9 @@ func TestSendPaymentErrorPathPruning(t *testing.T) { // We flip a bit in the payment hash to allow resending this payment. payment.paymentHash[1] ^= 1 - paymentPreImage, rt, err = ctx.router.SendPayment(payment) + paymentPreImage, rt, err = ctx.router.SendPayment( + t.Context(), payment, + ) require.NoErrorf(t, err, "unable to send payment: %v", payment.paymentHash) @@ -1300,7 +1320,7 @@ func TestUnknownErrorSource(t *testing.T) { // the route a->b->c is tried first. An unreadable faiure is returned // which should pruning the channel a->b. We expect the payment to // succeed via a->d. - _, _, err = ctx.router.SendPayment(payment) + _, _, err = ctx.router.SendPayment(t.Context(), payment) require.NoErrorf(t, err, "unable to send payment: %v", payment.paymentHash) @@ -1325,7 +1345,7 @@ func TestUnknownErrorSource(t *testing.T) { // Send off the payment request to the router. We expect the payment to // fail because both routes have been pruned. payment.paymentHash[1] ^= 1 - _, _, err = ctx.router.SendPayment(payment) + _, _, err = ctx.router.SendPayment(t.Context(), payment) if err == nil { t.Fatalf("expected payment to fail") } @@ -1421,7 +1441,9 @@ func TestSendToRouteStructuredError(t *testing.T) { // update should be received by router and ignored // because it is missing a valid // signature. - _, err = ctx.router.SendToRoute(payment, rt, nil) + _, err = ctx.router.SendToRoute( + t.Context(), payment, rt, nil, + ) fErr, ok := err.(*htlcswitch.ForwardingError) require.True( @@ -1500,7 +1522,7 @@ func TestSendToRouteMaxHops(t *testing.T) { // Send off the payment request to the router. We expect an error back // indicating that the route is too long. var payHash lntypes.Hash - _, err = ctx.router.SendToRoute(payHash, rt, nil) + _, err = ctx.router.SendToRoute(t.Context(), payHash, rt, nil) if err != route.ErrMaxRouteHopsExceeded { t.Fatalf("expected ErrMaxRouteHopsExceeded, but got %v", err) } @@ -2215,7 +2237,9 @@ func TestSendToRouteSkipTempErrSuccess(t *testing.T) { ).Return(nil) // Expect a successful send to route. - attempt, err := router.SendToRouteSkipTempErr(payHash, rt, nil) + attempt, err := router.SendToRouteSkipTempErr( + t.Context(), payHash, rt, nil, + ) require.NoError(t, err) require.Equal(t, testAttempt, attempt) @@ -2270,7 +2294,9 @@ func TestSendToRouteSkipTempErrNonMPP(t *testing.T) { }} // Expect an error to be returned. - attempt, err := router.SendToRouteSkipTempErr(payHash, rt, nil) + attempt, err := router.SendToRouteSkipTempErr( + t.Context(), payHash, rt, nil, + ) require.ErrorIs(t, ErrSkipTempErr, err) require.Nil(t, attempt) @@ -2350,7 +2376,9 @@ func TestSendToRouteSkipTempErrTempFailure(t *testing.T) { ).Return(nil, nil) // Expect a failed send to route. - attempt, err := router.SendToRouteSkipTempErr(payHash, rt, nil) + attempt, err := router.SendToRouteSkipTempErr( + t.Context(), payHash, rt, nil, + ) require.Equal(t, tempErr, err) require.Equal(t, testAttempt, attempt) @@ -2434,7 +2462,9 @@ func TestSendToRouteSkipTempErrPermanentFailure(t *testing.T) { ).Return(&failureReason, nil) // Expect a failed send to route. - attempt, err := router.SendToRouteSkipTempErr(payHash, rt, nil) + attempt, err := router.SendToRouteSkipTempErr( + t.Context(), payHash, rt, nil, + ) require.Equal(t, permErr, err) require.Equal(t, testAttempt, attempt) @@ -2523,7 +2553,7 @@ func TestSendToRouteTempFailure(t *testing.T) { ).Return(nil, nil) // Expect a failed send to route. - attempt, err := router.SendToRoute(payHash, rt, nil) + attempt, err := router.SendToRoute(t.Context(), payHash, rt, nil) require.Equal(t, tempErr, err) require.Equal(t, testAttempt, attempt) diff --git a/rpcserver.go b/rpcserver.go index 21563a82c5f..31081ada67d 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -477,6 +477,14 @@ func MainRPCServerPermissions() map[string][]bakery.Op { Entity: "offchain", Action: "read", }}, + "/lnrpc.Lightning/ListPaymentDuplicates": {{ + Entity: "offchain", + Action: "read", + }}, + "/lnrpc.Lightning/ListAllPaymentDuplicates": {{ + Entity: "offchain", + Action: "read", + }}, "/lnrpc.Lightning/DeletePayment": {{ Entity: "offchain", Action: "write", @@ -5547,8 +5555,9 @@ func (r *rpcServer) SubscribeChannelEvents(req *lnrpc.ChannelEventSubscription, // execute sendPayment. We use this struct as a sort of bridge to enable code // re-use between SendPayment and SendToRoute. type paymentStream struct { - recv func() (*rpcPaymentRequest, error) - send func(*lnrpc.SendResponse) error + getCtx func() context.Context + recv func() (*rpcPaymentRequest, error) + send func(*lnrpc.SendResponse) error } // rpcPaymentRequest wraps lnrpc.SendRequest so that routes from @@ -5562,10 +5571,13 @@ type rpcPaymentRequest struct { // through the Lightning Network. A single RPC invocation creates a persistent // bi-directional stream allowing clients to rapidly send payments through the // Lightning Network with a single persistent connection. -func (r *rpcServer) SendPayment(stream lnrpc.Lightning_SendPaymentServer) error { +func (r *rpcServer) SendPayment( + stream lnrpc.Lightning_SendPaymentServer) error { + var lock sync.Mutex return r.sendPayment(&paymentStream{ + getCtx: stream.Context, recv: func() (*rpcPaymentRequest, error) { req, err := stream.Recv() if err != nil { @@ -5590,10 +5602,13 @@ func (r *rpcServer) SendPayment(stream lnrpc.Lightning_SendPaymentServer) error // invocation creates a persistent bi-directional stream allowing clients to // rapidly send payments through the Lightning Network with a single persistent // connection. -func (r *rpcServer) SendToRoute(stream lnrpc.Lightning_SendToRouteServer) error { +func (r *rpcServer) SendToRoute( + stream lnrpc.Lightning_SendToRouteServer) error { + var lock sync.Mutex return r.sendPayment(&paymentStream{ + getCtx: stream.Context, recv: func() (*rpcPaymentRequest, error) { req, err := stream.Recv() if err != nil { @@ -5663,7 +5678,11 @@ type rpcPaymentIntent struct { // dispatch a client from the information presented by an RPC client. There are // three ways a client can specify their payment details: a payment request, // via manual details, or via a complete route. -func (r *rpcServer) extractPaymentIntent(rpcPayReq *rpcPaymentRequest) (rpcPaymentIntent, error) { +// +//nolint:funlen +func (r *rpcServer) extractPaymentIntent( + rpcPayReq *rpcPaymentRequest) (rpcPaymentIntent, error) { + payIntent := rpcPaymentIntent{} // If a route was specified, then we can use that directly. @@ -5935,7 +5954,7 @@ type paymentIntentResponse struct { // pre-built route. The first error this method returns denotes if we were // unable to save the payment. The second error returned denotes if the payment // didn't succeed. -func (r *rpcServer) dispatchPaymentIntent( +func (r *rpcServer) dispatchPaymentIntent(ctx context.Context, payIntent *rpcPaymentIntent) (*paymentIntentResponse, error) { // Construct a payment request to send to the channel router. If the @@ -5977,12 +5996,12 @@ func (r *rpcServer) dispatchPaymentIntent( } preImage, route, routerErr = r.server.chanRouter.SendPayment( - payment, + ctx, payment, ) } else { var attempt *paymentsdb.HTLCAttempt attempt, routerErr = r.server.chanRouter.SendToRoute( - payIntent.rHash, payIntent.route, nil, + ctx, payIntent.rHash, payIntent.route, nil, ) if routerErr == nil { @@ -6155,7 +6174,7 @@ sendLoop: }() resp, saveErr := r.dispatchPaymentIntent( - payIntent, + stream.getCtx(), payIntent, ) switch { @@ -6233,7 +6252,7 @@ sendLoop: func (r *rpcServer) SendPaymentSync(ctx context.Context, nextPayment *lnrpc.SendRequest) (*lnrpc.SendResponse, error) { - return r.sendPaymentSync(&rpcPaymentRequest{ + return r.sendPaymentSync(ctx, &rpcPaymentRequest{ SendRequest: nextPayment, }) } @@ -6254,12 +6273,12 @@ func (r *rpcServer) SendToRouteSync(ctx context.Context, return nil, err } - return r.sendPaymentSync(paymentRequest) + return r.sendPaymentSync(ctx, paymentRequest) } // sendPaymentSync is the synchronous variant of sendPayment. It will block and // wait until the payment has been fully completed. -func (r *rpcServer) sendPaymentSync( +func (r *rpcServer) sendPaymentSync(ctx context.Context, nextPayment *rpcPaymentRequest) (*lnrpc.SendResponse, error) { // We don't allow payments to be sent while the daemon itself is still @@ -6278,7 +6297,7 @@ func (r *rpcServer) sendPaymentSync( // With the payment validated, we'll now attempt to dispatch the // payment. - resp, saveErr := r.dispatchPaymentIntent(&payIntent) + resp, saveErr := r.dispatchPaymentIntent(ctx, &payIntent) switch { case saveErr != nil: return nil, saveErr @@ -7652,6 +7671,118 @@ func (r *rpcServer) ListPayments(ctx context.Context, return paymentsResp, nil } +// ListPaymentDuplicates returns duplicate payment records for a single payment +// hash. This is only supported by the SQL payments backend. +func (r *rpcServer) ListPaymentDuplicates(ctx context.Context, + req *lnrpc.ListPaymentDuplicatesRequest) ( + *lnrpc.ListPaymentDuplicatesResponse, error) { + + if len(req.PaymentHash) == 0 { + return nil, status.Error(codes.InvalidArgument, + "payment hash is required") + } + + paymentHash, err := lntypes.MakeHash(req.PaymentHash) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + fetcher, ok := r.server.paymentsDB.(paymentsdb.DuplicatePaymentsReader) + if !ok { + return nil, status.Error(codes.Unimplemented, + "duplicate payment records are only available for "+ + "SQL-based payment backends") + } + + duplicates, err := fetcher.FetchDuplicatePayments(ctx, paymentHash) + if err != nil { + return nil, err + } + + resp := &lnrpc.ListPaymentDuplicatesResponse{ + Duplicates: make([]*lnrpc.PaymentDuplicate, 0, len(duplicates)), + } + for _, dup := range duplicates { + rpcDup, err := paymentDuplicateToRPC(dup) + if err != nil { + return nil, err + } + + resp.Duplicates = append(resp.Duplicates, rpcDup) + } + + return resp, nil +} + +// ListAllPaymentDuplicates returns duplicate payment records across all +// payments. This is only supported by the SQL payments backend. +func (r *rpcServer) ListAllPaymentDuplicates(ctx context.Context, + req *lnrpc.ListAllPaymentDuplicatesRequest) ( + *lnrpc.ListAllPaymentDuplicatesResponse, error) { + + fetcher, ok := r.server.paymentsDB.(paymentsdb.DuplicatePaymentsReader) + if !ok { + return nil, status.Error(codes.Unimplemented, + "duplicate payment records are only available for "+ + "SQL-based payment backends") + } + + duplicates, err := fetcher.FetchAllDuplicatePayments(ctx) + if err != nil { + return nil, err + } + + resp := &lnrpc.ListAllPaymentDuplicatesResponse{ + Duplicates: make([]*lnrpc.PaymentDuplicate, 0, len(duplicates)), + } + for _, dup := range duplicates { + rpcDup, err := paymentDuplicateToRPC(dup) + if err != nil { + return nil, err + } + + resp.Duplicates = append(resp.Duplicates, rpcDup) + } + + return resp, nil +} + +func paymentDuplicateToRPC(dup *paymentsdb.DuplicatePayment) ( + *lnrpc.PaymentDuplicate, error) { + + if dup == nil { + return nil, fmt.Errorf("duplicate payment is nil") + } + + failureReason, err := routerrpc.MarshallPaymentFailureReason( + dup.FailureReason, + ) + if err != nil { + return nil, err + } + + rpcDup := &lnrpc.PaymentDuplicate{ + PaymentHash: dup.PaymentIdentifier[:], + ValueMsat: int64(dup.Amount), + CreationTimeNs: dup.CreationTime.UnixNano(), + FailureReason: failureReason, + PaymentPreimage: nil, + SettleTimeNs: 0, + } + + if dup.Settle != nil { + rpcDup.PaymentPreimage = dup.Settle.Preimage[:] + rpcDup.SettleTimeNs = dup.Settle.SettleTime.UnixNano() + } + + if dup.FailureReason == nil && dup.Settle == nil { + return nil, fmt.Errorf("duplicate payment missing " + + "failure reason and settlement") + } + + return rpcDup, nil +} + // DeleteCanceledInvoice remove a canceled invoice from the database. func (r *rpcServer) DeleteCanceledInvoice(ctx context.Context, req *lnrpc.DelCanceledInvoiceReq) (*lnrpc.DelCanceledInvoiceResp, @@ -7706,7 +7837,7 @@ func (r *rpcServer) DeletePayment(ctx context.Context, rpcsLog.Infof("[DeletePayment] payment_identifier=%v, "+ "failed_htlcs_only=%v", hash, req.FailedHtlcsOnly) - err = r.server.paymentsDB.DeletePayment(hash, req.FailedHtlcsOnly) + err = r.server.paymentsDB.DeletePayment(ctx, hash, req.FailedHtlcsOnly) if err != nil { return nil, err } @@ -7747,7 +7878,7 @@ func (r *rpcServer) DeleteAllPayments(ctx context.Context, req.FailedHtlcsOnly) numDeletedPayments, err := r.server.paymentsDB.DeletePayments( - req.FailedPaymentsOnly, req.FailedHtlcsOnly, + ctx, req.FailedPaymentsOnly, req.FailedHtlcsOnly, ) if err != nil { return nil, err diff --git a/server.go b/server.go index bd9422f2736..6b0b3b18f15 100644 --- a/server.go +++ b/server.go @@ -1017,20 +1017,21 @@ func newServer(ctx context.Context, cfg *Config, listenAddrs []net.Addr, } s.chanRouter, err = routing.New(routing.Config{ - SelfNode: nodePubKey, - RoutingGraph: dbs.GraphDB, - Chain: cc.ChainIO, - Payer: s.htlcSwitch, - Control: s.controlTower, - MissionControl: s.defaultMC, - SessionSource: paymentSessionSource, - GetLink: s.htlcSwitch.GetLinkByShortID, - NextPaymentID: sequencer.NextID, - PathFindingConfig: pathFindingConfig, - Clock: clock.NewDefaultClock(), - ApplyChannelUpdate: s.graphBuilder.ApplyChannelUpdate, - ClosedSCIDs: s.fetchClosedChannelSCIDs(), - TrafficShaper: implCfg.TrafficShaper, + SelfNode: nodePubKey, + RoutingGraph: dbs.GraphDB, + Chain: cc.ChainIO, + Payer: s.htlcSwitch, + Control: s.controlTower, + MissionControl: s.defaultMC, + SessionSource: paymentSessionSource, + GetLink: s.htlcSwitch.GetLinkByShortID, + NextPaymentID: sequencer.NextID, + PathFindingConfig: pathFindingConfig, + Clock: clock.NewDefaultClock(), + ApplyChannelUpdate: s.graphBuilder.ApplyChannelUpdate, + ClosedSCIDs: s.fetchClosedChannelSCIDs(), + TrafficShaper: implCfg.TrafficShaper, + KeepFailedPaymentAttempts: cfg.KeepFailedPaymentAttempts, }) if err != nil { return nil, fmt.Errorf("can't create router: %w", err) diff --git a/sqldb/migrations_dev.go b/sqldb/migrations_dev.go index a1b25019aee..e130fd52116 100644 --- a/sqldb/migrations_dev.go +++ b/sqldb/migrations_dev.go @@ -2,4 +2,24 @@ package sqldb -var migrationAdditions []MigrationConfig +var migrationAdditions = []MigrationConfig{ + { + Name: "000009_payments", + Version: 11, + SchemaVersion: 9, + }, + { + Name: "000010_payment_duplicates", + Version: 12, + SchemaVersion: 10, + }, + { + Name: "kv_payments_migration", + Version: 13, + SchemaVersion: 10, + // A migration function may be attached to this + // migration to migrate KV payments to the native SQL + // schema. This is optional and can be disabled by the + // user if necessary. + }, +} diff --git a/sqldb/sqlc/db_custom.go b/sqldb/sqlc/db_custom.go index d4feafe21b8..1b8d465e73c 100644 --- a/sqldb/sqlc/db_custom.go +++ b/sqldb/sqlc/db_custom.go @@ -167,3 +167,77 @@ func (r GetChannelsBySCIDRangeRow) Node1Pub() []byte { func (r GetChannelsBySCIDRangeRow) Node2Pub() []byte { return r.Node2PubKey } + +// PaymentAndIntent is an interface that provides access to a payment and its +// associated payment intent. +type PaymentAndIntent interface { + // GetPayment returns the Payment associated with this interface. + GetPayment() Payment + + // GetPaymentIntent returns the PaymentIntent associated with this payment. + GetPaymentIntent() PaymentIntent +} + +// GetPayment returns the Payment associated with this interface. +// +// NOTE: This method is part of the PaymentAndIntent interface. +func (r FilterPaymentsRow) GetPayment() Payment { + return r.Payment +} + +// GetPaymentIntent returns the PaymentIntent associated with this payment. +// If the payment has no intent (IntentType is NULL), this returns a zero-value +// PaymentIntent. +// +// NOTE: This method is part of the PaymentAndIntent interface. +func (r FilterPaymentsRow) GetPaymentIntent() PaymentIntent { + if !r.IntentType.Valid { + return PaymentIntent{} + } + return PaymentIntent{ + IntentType: r.IntentType.Int16, + IntentPayload: r.IntentPayload, + } +} + +// GetPayment returns the Payment associated with this interface. +// +// NOTE: This method is part of the PaymentAndIntent interface. +func (r FetchPaymentRow) GetPayment() Payment { + return r.Payment +} + +// GetPaymentIntent returns the PaymentIntent associated with this payment. +// If the payment has no intent (IntentType is NULL), this returns a zero-value +// PaymentIntent. +// +// NOTE: This method is part of the PaymentAndIntent interface. +func (r FetchPaymentRow) GetPaymentIntent() PaymentIntent { + if !r.IntentType.Valid { + return PaymentIntent{} + } + return PaymentIntent{ + IntentType: r.IntentType.Int16, + IntentPayload: r.IntentPayload, + } +} + +func (r FetchPaymentsByIDsRow) GetPayment() Payment { + return Payment{ + ID: r.ID, + AmountMsat: r.AmountMsat, + CreatedAt: r.CreatedAt, + PaymentIdentifier: r.PaymentIdentifier, + FailReason: r.FailReason, + } +} + +func (r FetchPaymentsByIDsRow) GetPaymentIntent() PaymentIntent { + if !r.IntentType.Valid { + return PaymentIntent{} + } + return PaymentIntent{ + IntentType: r.IntentType.Int16, + IntentPayload: r.IntentPayload, + } +} diff --git a/sqldb/sqlc/migrations/000009_payments.down.sql b/sqldb/sqlc/migrations/000009_payments.down.sql new file mode 100644 index 00000000000..62b19cb991e --- /dev/null +++ b/sqldb/sqlc/migrations/000009_payments.down.sql @@ -0,0 +1,54 @@ +-- ───────────────────────────────────────────── +-- Drop custom TLV record tables first (they have no dependents). +-- ───────────────────────────────────────────── + +DROP TABLE IF EXISTS payment_hop_custom_records; +DROP TABLE IF EXISTS payment_attempt_first_hop_custom_records; +DROP TABLE IF EXISTS payment_first_hop_custom_records; + +-- ───────────────────────────────────────────── +-- Drop per-hop payload tables before dropping the base hops table. +-- ───────────────────────────────────────────── + +DROP TABLE IF EXISTS payment_route_hop_blinded; +DROP TABLE IF EXISTS payment_route_hop_amp; +DROP TABLE IF EXISTS payment_route_hop_mpp; + +-- ───────────────────────────────────────────── +-- Drop route hops table and its indexes. +-- ───────────────────────────────────────────── + +DROP INDEX IF EXISTS idx_route_hops_htlc_attempt_index; +DROP TABLE IF EXISTS payment_route_hops; + +-- ───────────────────────────────────────────── +-- Drop HTLC attempt resolution table and its indexes. +-- ───────────────────────────────────────────── + +DROP INDEX IF EXISTS idx_htlc_resolutions_type; +DROP INDEX IF EXISTS idx_htlc_resolutions_time; +DROP TABLE IF EXISTS payment_htlc_attempt_resolutions; + +-- ───────────────────────────────────────────── +-- Drop HTLC attempts table and its indexes. +-- ───────────────────────────────────────────── + +DROP INDEX IF EXISTS idx_htlc_payment_id; +DROP INDEX IF EXISTS idx_htlc_attempt_index; +DROP INDEX IF EXISTS idx_htlc_payment_hash; +DROP INDEX IF EXISTS idx_htlc_attempt_time; +DROP TABLE IF EXISTS payment_htlc_attempts; + +-- ───────────────────────────────────────────── +-- Drop payments table and its indexes. +-- ───────────────────────────────────────────── + +DROP INDEX IF EXISTS idx_payments_created_at; +DROP TABLE IF EXISTS payments; + +-- ───────────────────────────────────────────── +-- Drop payment intents table and its indexes. +-- ───────────────────────────────────────────── + +DROP INDEX IF EXISTS idx_payment_intents_type; +DROP TABLE IF EXISTS payment_intents; \ No newline at end of file diff --git a/sqldb/sqlc/migrations/000009_payments.up.sql b/sqldb/sqlc/migrations/000009_payments.up.sql new file mode 100644 index 00000000000..65094a15e4c --- /dev/null +++ b/sqldb/sqlc/migrations/000009_payments.up.sql @@ -0,0 +1,443 @@ +-- ───────────────────────────────────────────── +-- Payment System Schema Migration +-- ───────────────────────────────────────────── +-- This migration creates the complete payment system schema including: +-- - Payment intents (only BOLT 11 invoices for now) +-- - Payment attempts and HTLC tracking +-- - Route hops and custom TLV records +-- - Resolution tracking for settled/failed payments +-- ───────────────────────────────────────────── + +-- ───────────────────────────────────────────── +-- Payments Table +-- ───────────────────────────────────────────── +-- Stores all payments including all known payment types: +-- - Legacy payments +-- - Multi-Path Payments (MPP) +-- - Atomic Multi-Path Payments (AMP) +-- - Blinded payments +-- - Keysend payments +-- - Spontaneous AMP payments +-- ───────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS payments ( + -- Primary key for the payment record + id INTEGER PRIMARY KEY, + + -- The amount of the payment in millisatoshis + amount_msat BIGINT NOT NULL, + + -- Timestamp when the payment was created + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + + -- Logical identifier for the payment + -- For legacy + MPP: matches the HTLC hash + -- For AMP: the setID + -- For future intent types: any unique payment-level key + payment_identifier BLOB NOT NULL, + + -- The reason for payment failure (only set if payment has failed) + -- Integer enum type indicating failure reason + fail_reason INTEGER, + + -- Ensure payment identifiers are unique across all payments + CONSTRAINT idx_payments_payment_identifier_unique + UNIQUE (payment_identifier) +); + +-- Index for efficient querying by creation time (for chronological ordering) +CREATE INDEX IF NOT EXISTS idx_payments_created_at +ON payments(created_at); + +-- ───────────────────────────────────────────── +-- Payment Intents Table +-- ───────────────────────────────────────────── +-- Stores the descriptor of what the payment is paying for. +-- Depending on the type, the payload might contain: +-- - BOLT 11 invoice data +-- - BOLT 12 offer data +-- - NULL for legacy hash-only/keysend style payments +-- ───────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS payment_intents ( + -- Primary key for the intent record + id INTEGER PRIMARY KEY, + + -- Reference to the payment this intent belongs to (one-to-one relationship) + -- When the payment is deleted, the intent is automatically deleted + payment_id BIGINT NOT NULL REFERENCES payments (id) ON DELETE CASCADE, + + -- The type of intent (e.g. 0 = bolt11_invoice, 1 = bolt12_invoice) + -- Uses SMALLINT (int16) for efficient storage of enum values + intent_type SMALLINT NOT NULL, + + -- The serialized payload for the payment intent + -- Content depends on type - could be invoice, offer, or NULL + intent_payload BLOB, + + -- Ensure one-to-one relationship: each payment has at most one intent. + -- Currently we only support one intent per payment this makes sure we do + -- not accidentally pay the same request multiple times. This currently + -- only has bolt 11 payment requests/invoices. But in the future this can + -- also include BOLT 12 offers/invoices. + CONSTRAINT idx_payment_intents_payment_id_unique + UNIQUE (payment_id) +); + +-- Index for efficient querying by intent type +CREATE INDEX IF NOT EXISTS idx_payment_intents_type +ON payment_intents(intent_type); + +-- ───────────────────────────────────────────── +-- Payment HTLC Attempts Table +-- ───────────────────────────────────────────── +-- Stores all HTLC attempts for a payment. A payment can have multiple +-- HTLC attempts depending on whether the payment is split and also +-- if some attempts fail and need to be retried. +-- ───────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS payment_htlc_attempts ( + -- Primary key for the HTLC attempt record + id INTEGER PRIMARY KEY, + + -- The index of the HTLC attempt + -- TODO: This will be removed and the primary key will be used only + attempt_index BIGINT NOT NULL, + + -- Reference to the parent payment + payment_id BIGINT NOT NULL REFERENCES payments (id) ON DELETE CASCADE, + + -- The session key of the HTLC attempt (also known as ephemeral key + -- of the Sphinx packet used for onion routing) + session_key BLOB NOT NULL, + + -- Timestamp when the HTLC attempt was created + attempt_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + + -- The payment hash for the payment attempt + -- The hash the HTLC will be locked to - this does not need to be + -- equal to the payment level identifier (e.g., for AMP payments) + payment_hash BLOB NOT NULL, + + -- First hop amount in millisatoshis of the HTLC attempt + -- Normally the same as the total amount of the route, but when using + -- custom channels this might be different + first_hop_amount_msat BIGINT NOT NULL, + + -- ───────────────────────────────────────────── + -- Route Information for the HTLC Attempt + -- ───────────────────────────────────────────── + -- Every attempt has one route, so there is a 1:1 relationship between + -- attempts and routes. The route itself can be found in the hops table. + -- ───────────────────────────────────────────── + + -- The total time lock of the route (in blocks) + route_total_time_lock INTEGER NOT NULL, + + -- The total amount of the route in millisatoshis + route_total_amount BIGINT NOT NULL, + + -- The source key of the route (our node's public key) + route_source_key BLOB NOT NULL, + + -- Ensure attempt indices are unique across all attempts + CONSTRAINT idx_htlc_attempt_index_unique + UNIQUE (attempt_index), + + -- Ensure session keys are unique (each attempt has unique session key) + CONSTRAINT idx_htlc_session_key_unique + UNIQUE (session_key) +); + +-- Index for efficient querying by payment ID (find all attempts for a payment) +CREATE INDEX IF NOT EXISTS idx_htlc_payment_id +ON payment_htlc_attempts(payment_id); + +-- Index for efficient querying by attempt index (for lookups and joins) +CREATE INDEX IF NOT EXISTS idx_htlc_attempt_index +ON payment_htlc_attempts(attempt_index); + +-- Index for efficient querying by payment hash (for HTLC matching) +CREATE INDEX IF NOT EXISTS idx_htlc_payment_hash +ON payment_htlc_attempts(payment_hash); + +-- Index for efficient querying by attempt time (for chronological ordering) +CREATE INDEX IF NOT EXISTS idx_htlc_attempt_time +ON payment_htlc_attempts(attempt_time); + +-- ───────────────────────────────────────────── +-- HTLC Attempt Resolutions Table +-- ───────────────────────────────────────────── +-- Stores resolution metadata for HTLC attempts. Rows appear once an +-- attempt settles or fails, providing the final outcome and timing. +-- ───────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS payment_htlc_attempt_resolutions ( + -- Primary key referencing the HTLC attempt + -- TODO: This will be removed and the primary key will be used only + attempt_index INTEGER PRIMARY KEY + REFERENCES payment_htlc_attempts (attempt_index) ON DELETE CASCADE, + + -- Timestamp when the attempt was resolved (settled or failed) + resolution_time TIMESTAMP NOT NULL, + + -- Outcome of the attempt: 1 = settled, 2 = failed + resolution_type INTEGER NOT NULL CHECK (resolution_type IN (1, 2)), + + -- Settlement payload (only populated for settled attempts) + -- Contains the preimage that proves payment completion + settle_preimage BLOB, + + -- Failure payload (only populated for failed attempts) + -- Index of the node that sent the failure + failure_source_index INTEGER, + + -- HTLC failure reason code + htlc_fail_reason INTEGER, + + -- Failure message from the failing node, this message is binary encoded + -- using the lightning wire protocol, see also lnwire/onion_error.go + failure_msg BLOB, + + -- Ensure data integrity: settled attempts must have preimage, + -- failed attempts must not have preimage + CHECK ( + (resolution_type = 1 AND settle_preimage IS NOT NULL AND + failure_source_index IS NULL AND htlc_fail_reason IS NULL AND + failure_msg IS NULL) + OR + (resolution_type = 2 AND settle_preimage IS NULL) + ) +); + +-- Index for efficient querying by resolution type (settled vs failed) +CREATE INDEX IF NOT EXISTS idx_htlc_resolutions_type +ON payment_htlc_attempt_resolutions(resolution_type); + +-- Index for efficient querying by resolution time (for chronological analysis) +CREATE INDEX IF NOT EXISTS idx_htlc_resolutions_time +ON payment_htlc_attempt_resolutions(resolution_time); + +-- ───────────────────────────────────────────── +-- Payment Route Hops Table +-- ───────────────────────────────────────────── +-- Stores the individual hops of a payment route. An attempt has only +-- one route, but a route can consist of several hops through the network. +-- ───────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS payment_route_hops ( + -- Primary key for the hop record + id INTEGER PRIMARY KEY, + + -- Reference to the HTLC attempt this hop belongs to + htlc_attempt_index BIGINT NOT NULL + REFERENCES payment_htlc_attempts (attempt_index) ON DELETE CASCADE, + + -- The order/index of this hop within the route (0-based) + hop_index INTEGER NOT NULL, + + -- The public key of the hop (node's public key) + pub_key BLOB, + + -- The short channel ID of the hop (channel identifier) + scid TEXT NOT NULL, + + -- The outgoing time lock of the hop (in blocks) + outgoing_time_lock INTEGER NOT NULL, + + -- The amount to forward to the next hop (in millisatoshis) + amt_to_forward BIGINT NOT NULL, + + -- The metadata blob transmitted to the hop (onion payload) + meta_data BLOB, + + -- Ensure each attempt can only have one hop at each hop index + -- This prevents duplicate hops in the same position + CONSTRAINT idx_route_hops_unique_hop_per_attempt + UNIQUE (htlc_attempt_index, hop_index) +); + +-- Index for efficient querying by attempt index (find all hops for an attempt) +CREATE INDEX IF NOT EXISTS idx_route_hops_htlc_attempt_index +ON payment_route_hops(htlc_attempt_index); + +-- ───────────────────────────────────────────── +-- Per-Hop Payload Tables +-- ───────────────────────────────────────────── +-- These tables store specialized payload data for different payment types. +-- Each table is only populated for hops that require that specific payload. +-- ───────────────────────────────────────────── + +-- ───────────────────────────────────────────── +-- MPP (Multi-Path Payment) Payload Table +-- ───────────────────────────────────────────── +-- Stores MPP-specific payload data. Only present for the final hop +-- of an MPP attempt, containing payment address and total amount info. +-- ───────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS payment_route_hop_mpp ( + -- Primary key referencing the hop + hop_id INTEGER PRIMARY KEY + REFERENCES payment_route_hops (id) ON DELETE CASCADE, + + -- The payment address of the MPP path (for payment correlation) + payment_addr BLOB NOT NULL, + + -- The total amount of the MPP payment in millisatoshis + -- This is the sum of all parts in the multi-path payment + total_msat BIGINT NOT NULL +); + +-- ───────────────────────────────────────────── +-- AMP (Atomic Multi-Path Payment) Payload Table +-- ───────────────────────────────────────────── +-- Stores AMP-specific payload data. Only present for the final hop +-- of an AMP attempt, containing share information for atomicity. +-- ───────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS payment_route_hop_amp ( + -- Primary key referencing the hop + hop_id INTEGER PRIMARY KEY + REFERENCES payment_route_hops (id) ON DELETE CASCADE, + + -- The root share of the AMP path (for share reconstruction) + root_share BLOB NOT NULL, + + -- The set ID of the AMP path (groups related AMP parts) + set_id BLOB NOT NULL, + + -- The child index of the AMP path (identifies this part) + child_index INTEGER NOT NULL +); + +-- ───────────────────────────────────────────── +-- Blinded Route Payload Table +-- ───────────────────────────────────────────── +-- Stores blinded route payload data. Rows only exist for hops that +-- are part of a blinded path, providing privacy-preserving routing. +-- ───────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS payment_route_hop_blinded ( + -- Primary key referencing the hop + hop_id INTEGER PRIMARY KEY + REFERENCES payment_route_hops (id) ON DELETE CASCADE, + + -- The encrypted payload for the blinded hop + encrypted_data BLOB NOT NULL, + + -- Only set for the introduction point of the blinded path + -- Contains the blinding point for the introduction node + blinding_point BLOB, + + -- Only set for the final hop in the blinded path + -- Contains the total amount for the entire blinded path + blinded_path_total_amt BIGINT +); + +-- ───────────────────────────────────────────── +-- Custom TLV Records Tables +-- ───────────────────────────────────────────── +-- These tables store custom TLV (Type-Length-Value) records associated +-- with payments, attempts, and hops. This is a denormalized structure +-- designed to simplify cascade deletions, as each record is owned by +-- a single parent entity. +-- ───────────────────────────────────────────── + +-- ───────────────────────────────────────────── +-- Payment-Level First Hop Custom Records +-- ───────────────────────────────────────────── +-- Stores custom TLV records that are part of the first hop of a payment. +-- These records are sent to the first hop and are payment-level data. +-- +-- NOTE: This relates to the custom tlv record data which is sent to the first +-- hop in the wire message (UpdateAddHTLC) NOT the onion packet. +-- +-- TODO(ziggie): We store mostly redundant data here and on the attempt level. +-- This might be improved in the future to reduce duplication. +-- ───────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS payment_first_hop_custom_records ( + -- Primary key for the custom record + id INTEGER PRIMARY KEY, + + -- Reference to the parent payment + payment_id BIGINT NOT NULL REFERENCES payments (id) ON DELETE CASCADE, + + -- The TLV type identifier (must be >= 65536 for custom records) + key BIGINT NOT NULL, + + -- The TLV value data + value BLOB NOT NULL, + + -- Ensure we only store custom TLV records (not standard ones) + CHECK (key >= 65536), + + -- Ensure each payment can only have one record per TLV type + CONSTRAINT idx_payment_first_hop_custom_records_unique + UNIQUE (payment_id, key) +); + +-- ───────────────────────────────────────────── +-- Attempt-Level First Hop Custom Records +-- ───────────────────────────────────────────── +-- Stores custom TLV records for the first hop on the route level. +-- These might be different from the payment-level first hop records +-- in case of custom channels or route-specific modifications. +-- +-- NOTE: This relates to the custom tlv record data which is sent to the first +-- hop in the wire message (UpdateAddHTLC) NOT the onion packet. +-- +-- TODO(ziggie): We store mostly redundant data here and on the payment level. +-- This might be improved in the future to reduce duplication. +-- ───────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS payment_attempt_first_hop_custom_records ( + -- Primary key for the custom record + id INTEGER PRIMARY KEY, + + -- Reference to the parent HTLC attempt + htlc_attempt_index BIGINT NOT NULL + REFERENCES payment_htlc_attempts (attempt_index) ON DELETE CASCADE, + + -- The TLV type identifier (must be >= 65536 for custom records) + key BIGINT NOT NULL, + + -- The TLV value data + value BLOB NOT NULL, + + -- Ensure we only store custom TLV records (not standard ones) + CHECK (key >= 65536), + + -- Ensure each attempt can only have one record per TLV type + CONSTRAINT idx_payment_attempt_first_hop_custom_records_unique + UNIQUE (htlc_attempt_index, key) +); + +-- ───────────────────────────────────────────── +-- Hop-Level Custom Records +-- ───────────────────────────────────────────── +-- Stores custom TLV records associated with a specific hop within +-- a payment route. These records are sent to that specific hop +-- and are hop-specific data. +-- ───────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS payment_hop_custom_records ( + -- Primary key for the custom record + id INTEGER PRIMARY KEY, + + -- Reference to the parent hop + hop_id BIGINT NOT NULL REFERENCES payment_route_hops (id) ON DELETE CASCADE, + + -- The TLV type identifier (must be >= 65536 for custom records) + key BIGINT NOT NULL, + + -- The TLV value data + value BLOB NOT NULL, + + -- Ensure we only store custom TLV records (not standard ones) + CHECK (key >= 65536), + + -- Ensure each hop can only have one record per TLV type + CONSTRAINT idx_payment_hop_custom_records_unique + UNIQUE (hop_id, key) +); \ No newline at end of file diff --git a/sqldb/sqlc/migrations/000010_payment_duplicates.down.sql b/sqldb/sqlc/migrations/000010_payment_duplicates.down.sql new file mode 100644 index 00000000000..39c4a313a46 --- /dev/null +++ b/sqldb/sqlc/migrations/000010_payment_duplicates.down.sql @@ -0,0 +1,2 @@ +DROP INDEX IF EXISTS idx_payment_duplicates_payment_id; +DROP TABLE IF EXISTS payment_duplicates; diff --git a/sqldb/sqlc/migrations/000010_payment_duplicates.up.sql b/sqldb/sqlc/migrations/000010_payment_duplicates.up.sql new file mode 100644 index 00000000000..21397f63c8f --- /dev/null +++ b/sqldb/sqlc/migrations/000010_payment_duplicates.up.sql @@ -0,0 +1,41 @@ +-- ───────────────────────────────────────────── +-- Payment Duplicate Records Table +-- ───────────────────────────────────────────── +-- Stores duplicate payment records that were created in older versions +-- of lnd. This table is intentionally minimal and is expected to be +-- temporary (dropped after KV migrations complete). +-- ───────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS payment_duplicates ( + -- Primary key for the duplicate record + id INTEGER PRIMARY KEY, + + -- Reference to the primary payment this duplicate belongs to + payment_id BIGINT NOT NULL REFERENCES payments (id) ON DELETE CASCADE, + + -- Logical identifier for the duplicate payment + payment_identifier BLOB NOT NULL, + + -- Amount of the duplicate payment in millisatoshis + amount_msat BIGINT NOT NULL, + + -- Timestamp when the duplicate payment was created + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + + -- Failure reason for failed payments (if known) + fail_reason INTEGER, + + -- Settlement payload for succeeded payments (if known) + settle_preimage BLOB, + + -- Settlement time for succeeded payments (if known) + settle_time TIMESTAMP, + + -- Ensure we record either a failure reason or settlement data + CONSTRAINT chk_payment_duplicates_outcome + CHECK (fail_reason IS NOT NULL OR settle_preimage IS NOT NULL) +); + +-- Index for efficient lookup by primary payment +CREATE INDEX IF NOT EXISTS idx_payment_duplicates_payment_id +ON payment_duplicates(payment_id); diff --git a/sqldb/sqlc/models.go b/sqldb/sqlc/models.go index 24df0d680cd..6fa20d4a4ae 100644 --- a/sqldb/sqlc/models.go +++ b/sqldb/sqlc/models.go @@ -202,3 +202,104 @@ type MigrationTracker struct { Version int32 MigrationTime time.Time } + +type Payment struct { + ID int64 + AmountMsat int64 + CreatedAt time.Time + PaymentIdentifier []byte + FailReason sql.NullInt32 +} + +type PaymentAttemptFirstHopCustomRecord struct { + ID int64 + HtlcAttemptIndex int64 + Key int64 + Value []byte +} + +type PaymentDuplicate struct { + ID int64 + PaymentID int64 + PaymentIdentifier []byte + AmountMsat int64 + CreatedAt time.Time + FailReason sql.NullInt32 + SettlePreimage []byte + SettleTime sql.NullTime +} + +type PaymentFirstHopCustomRecord struct { + ID int64 + PaymentID int64 + Key int64 + Value []byte +} + +type PaymentHopCustomRecord struct { + ID int64 + HopID int64 + Key int64 + Value []byte +} + +type PaymentHtlcAttempt struct { + ID int64 + AttemptIndex int64 + PaymentID int64 + SessionKey []byte + AttemptTime time.Time + PaymentHash []byte + FirstHopAmountMsat int64 + RouteTotalTimeLock int32 + RouteTotalAmount int64 + RouteSourceKey []byte +} + +type PaymentHtlcAttemptResolution struct { + AttemptIndex int64 + ResolutionTime time.Time + ResolutionType int32 + SettlePreimage []byte + FailureSourceIndex sql.NullInt32 + HtlcFailReason sql.NullInt32 + FailureMsg []byte +} + +type PaymentIntent struct { + ID int64 + PaymentID int64 + IntentType int16 + IntentPayload []byte +} + +type PaymentRouteHop struct { + ID int64 + HtlcAttemptIndex int64 + HopIndex int32 + PubKey []byte + Scid string + OutgoingTimeLock int32 + AmtToForward int64 + MetaData []byte +} + +type PaymentRouteHopAmp struct { + HopID int64 + RootShare []byte + SetID []byte + ChildIndex int32 +} + +type PaymentRouteHopBlinded struct { + HopID int64 + EncryptedData []byte + BlindingPoint []byte + BlindedPathTotalAmt sql.NullInt64 +} + +type PaymentRouteHopMpp struct { + HopID int64 + PaymentAddr []byte + TotalMsat int64 +} diff --git a/sqldb/sqlc/payments.sql.go b/sqldb/sqlc/payments.sql.go new file mode 100644 index 00000000000..a57f4a13717 --- /dev/null +++ b/sqldb/sqlc/payments.sql.go @@ -0,0 +1,1287 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: payments.sql + +package sqlc + +import ( + "context" + "database/sql" + "strings" + "time" +) + +const countPayments = `-- name: CountPayments :one +SELECT COUNT(*) FROM payments +` + +func (q *Queries) CountPayments(ctx context.Context) (int64, error) { + row := q.db.QueryRowContext(ctx, countPayments) + var count int64 + err := row.Scan(&count) + return count, err +} + +const deleteFailedAttempts = `-- name: DeleteFailedAttempts :exec +DELETE FROM payment_htlc_attempts WHERE payment_id = $1 AND attempt_index IN ( + SELECT attempt_index FROM payment_htlc_attempt_resolutions WHERE resolution_type = 2 +) +` + +// Delete all failed HTLC attempts for the given payment. Resolution type 2 +// indicates a failed attempt. +func (q *Queries) DeleteFailedAttempts(ctx context.Context, paymentID int64) error { + _, err := q.db.ExecContext(ctx, deleteFailedAttempts, paymentID) + return err +} + +const deletePayment = `-- name: DeletePayment :exec +DELETE FROM payments WHERE id = $1 +` + +func (q *Queries) DeletePayment(ctx context.Context, id int64) error { + _, err := q.db.ExecContext(ctx, deletePayment, id) + return err +} + +const failAttempt = `-- name: FailAttempt :exec +INSERT INTO payment_htlc_attempt_resolutions ( + attempt_index, + resolution_time, + resolution_type, + failure_source_index, + htlc_fail_reason, + failure_msg +) +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6 +) +` + +type FailAttemptParams struct { + AttemptIndex int64 + ResolutionTime time.Time + ResolutionType int32 + FailureSourceIndex sql.NullInt32 + HtlcFailReason sql.NullInt32 + FailureMsg []byte +} + +func (q *Queries) FailAttempt(ctx context.Context, arg FailAttemptParams) error { + _, err := q.db.ExecContext(ctx, failAttempt, + arg.AttemptIndex, + arg.ResolutionTime, + arg.ResolutionType, + arg.FailureSourceIndex, + arg.HtlcFailReason, + arg.FailureMsg, + ) + return err +} + +const failPayment = `-- name: FailPayment :execresult +UPDATE payments SET fail_reason = $1 WHERE payment_identifier = $2 +` + +type FailPaymentParams struct { + FailReason sql.NullInt32 + PaymentIdentifier []byte +} + +func (q *Queries) FailPayment(ctx context.Context, arg FailPaymentParams) (sql.Result, error) { + return q.db.ExecContext(ctx, failPayment, arg.FailReason, arg.PaymentIdentifier) +} + +const fetchAllInflightAttempts = `-- name: FetchAllInflightAttempts :many +SELECT + ha.id, + ha.attempt_index, + ha.payment_id, + ha.session_key, + ha.attempt_time, + ha.payment_hash, + ha.first_hop_amount_msat, + ha.route_total_time_lock, + ha.route_total_amount, + ha.route_source_key +FROM payment_htlc_attempts ha +WHERE NOT EXISTS ( + SELECT 1 FROM payment_htlc_attempt_resolutions hr + WHERE hr.attempt_index = ha.attempt_index +) +AND ha.attempt_index > $1 +ORDER BY ha.attempt_index ASC +LIMIT $2 +` + +type FetchAllInflightAttemptsParams struct { + AttemptIndex int64 + Limit int32 +} + +// Fetch all inflight attempts with their payment data using pagination. +// Returns attempt data joined with payment and intent data to avoid separate queries. +func (q *Queries) FetchAllInflightAttempts(ctx context.Context, arg FetchAllInflightAttemptsParams) ([]PaymentHtlcAttempt, error) { + rows, err := q.db.QueryContext(ctx, fetchAllInflightAttempts, arg.AttemptIndex, arg.Limit) + if err != nil { + return nil, err + } + defer rows.Close() + var items []PaymentHtlcAttempt + for rows.Next() { + var i PaymentHtlcAttempt + if err := rows.Scan( + &i.ID, + &i.AttemptIndex, + &i.PaymentID, + &i.SessionKey, + &i.AttemptTime, + &i.PaymentHash, + &i.FirstHopAmountMsat, + &i.RouteTotalTimeLock, + &i.RouteTotalAmount, + &i.RouteSourceKey, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const fetchAllPaymentDuplicates = `-- name: FetchAllPaymentDuplicates :many +SELECT + id, + payment_id, + payment_identifier, + amount_msat, + created_at, + fail_reason, + settle_preimage, + settle_time +FROM payment_duplicates +WHERE ( + payment_id > $1 OR + (payment_id = $1 AND id > $2) +) +ORDER BY payment_id ASC, id ASC +LIMIT $3 +` + +type FetchAllPaymentDuplicatesParams struct { + AfterPaymentID int64 + AfterID int64 + NumLimit int32 +} + +// Fetch duplicate payment records ordered by payment_id and id. +func (q *Queries) FetchAllPaymentDuplicates(ctx context.Context, arg FetchAllPaymentDuplicatesParams) ([]PaymentDuplicate, error) { + rows, err := q.db.QueryContext(ctx, fetchAllPaymentDuplicates, arg.AfterPaymentID, arg.AfterID, arg.NumLimit) + if err != nil { + return nil, err + } + defer rows.Close() + var items []PaymentDuplicate + for rows.Next() { + var i PaymentDuplicate + if err := rows.Scan( + &i.ID, + &i.PaymentID, + &i.PaymentIdentifier, + &i.AmountMsat, + &i.CreatedAt, + &i.FailReason, + &i.SettlePreimage, + &i.SettleTime, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const fetchHopLevelCustomRecords = `-- name: FetchHopLevelCustomRecords :many +SELECT + l.id, + l.hop_id, + l.key, + l.value +FROM payment_hop_custom_records l +WHERE l.hop_id IN (/*SLICE:hop_ids*/?) +ORDER BY l.hop_id ASC, l.key ASC +` + +func (q *Queries) FetchHopLevelCustomRecords(ctx context.Context, hopIds []int64) ([]PaymentHopCustomRecord, error) { + query := fetchHopLevelCustomRecords + var queryParams []interface{} + if len(hopIds) > 0 { + for _, v := range hopIds { + queryParams = append(queryParams, v) + } + query = strings.Replace(query, "/*SLICE:hop_ids*/?", makeQueryParams(len(queryParams), len(hopIds)), 1) + } else { + query = strings.Replace(query, "/*SLICE:hop_ids*/?", "NULL", 1) + } + rows, err := q.db.QueryContext(ctx, query, queryParams...) + if err != nil { + return nil, err + } + defer rows.Close() + var items []PaymentHopCustomRecord + for rows.Next() { + var i PaymentHopCustomRecord + if err := rows.Scan( + &i.ID, + &i.HopID, + &i.Key, + &i.Value, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const fetchHopsForAttempts = `-- name: FetchHopsForAttempts :many +SELECT + h.id, + h.htlc_attempt_index, + h.hop_index, + h.pub_key, + h.scid, + h.outgoing_time_lock, + h.amt_to_forward, + h.meta_data, + m.payment_addr AS mpp_payment_addr, + m.total_msat AS mpp_total_msat, + a.root_share AS amp_root_share, + a.set_id AS amp_set_id, + a.child_index AS amp_child_index, + b.encrypted_data, + b.blinding_point, + b.blinded_path_total_amt +FROM payment_route_hops h +LEFT JOIN payment_route_hop_mpp m ON m.hop_id = h.id +LEFT JOIN payment_route_hop_amp a ON a.hop_id = h.id +LEFT JOIN payment_route_hop_blinded b ON b.hop_id = h.id +WHERE h.htlc_attempt_index IN (/*SLICE:htlc_attempt_indices*/?) +ORDER BY h.htlc_attempt_index ASC, h.hop_index ASC +` + +type FetchHopsForAttemptsRow struct { + ID int64 + HtlcAttemptIndex int64 + HopIndex int32 + PubKey []byte + Scid string + OutgoingTimeLock int32 + AmtToForward int64 + MetaData []byte + MppPaymentAddr []byte + MppTotalMsat sql.NullInt64 + AmpRootShare []byte + AmpSetID []byte + AmpChildIndex sql.NullInt32 + EncryptedData []byte + BlindingPoint []byte + BlindedPathTotalAmt sql.NullInt64 +} + +func (q *Queries) FetchHopsForAttempts(ctx context.Context, htlcAttemptIndices []int64) ([]FetchHopsForAttemptsRow, error) { + query := fetchHopsForAttempts + var queryParams []interface{} + if len(htlcAttemptIndices) > 0 { + for _, v := range htlcAttemptIndices { + queryParams = append(queryParams, v) + } + query = strings.Replace(query, "/*SLICE:htlc_attempt_indices*/?", makeQueryParams(len(queryParams), len(htlcAttemptIndices)), 1) + } else { + query = strings.Replace(query, "/*SLICE:htlc_attempt_indices*/?", "NULL", 1) + } + rows, err := q.db.QueryContext(ctx, query, queryParams...) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FetchHopsForAttemptsRow + for rows.Next() { + var i FetchHopsForAttemptsRow + if err := rows.Scan( + &i.ID, + &i.HtlcAttemptIndex, + &i.HopIndex, + &i.PubKey, + &i.Scid, + &i.OutgoingTimeLock, + &i.AmtToForward, + &i.MetaData, + &i.MppPaymentAddr, + &i.MppTotalMsat, + &i.AmpRootShare, + &i.AmpSetID, + &i.AmpChildIndex, + &i.EncryptedData, + &i.BlindingPoint, + &i.BlindedPathTotalAmt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const fetchHtlcAttemptResolutionsForPayments = `-- name: FetchHtlcAttemptResolutionsForPayments :many +SELECT + ha.payment_id, + hr.resolution_type +FROM payment_htlc_attempts ha +LEFT JOIN payment_htlc_attempt_resolutions hr ON hr.attempt_index = ha.attempt_index +WHERE ha.payment_id IN (/*SLICE:payment_ids*/?) +` + +type FetchHtlcAttemptResolutionsForPaymentsRow struct { + PaymentID int64 + ResolutionType sql.NullInt32 +} + +// Batch query to fetch only HTLC resolution status for multiple payments. +// We don't need to order by payment_id and attempt_time because we will +// group the resolutions by payment_id in the background. +func (q *Queries) FetchHtlcAttemptResolutionsForPayments(ctx context.Context, paymentIds []int64) ([]FetchHtlcAttemptResolutionsForPaymentsRow, error) { + query := fetchHtlcAttemptResolutionsForPayments + var queryParams []interface{} + if len(paymentIds) > 0 { + for _, v := range paymentIds { + queryParams = append(queryParams, v) + } + query = strings.Replace(query, "/*SLICE:payment_ids*/?", makeQueryParams(len(queryParams), len(paymentIds)), 1) + } else { + query = strings.Replace(query, "/*SLICE:payment_ids*/?", "NULL", 1) + } + rows, err := q.db.QueryContext(ctx, query, queryParams...) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FetchHtlcAttemptResolutionsForPaymentsRow + for rows.Next() { + var i FetchHtlcAttemptResolutionsForPaymentsRow + if err := rows.Scan(&i.PaymentID, &i.ResolutionType); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const fetchHtlcAttemptsForPayments = `-- name: FetchHtlcAttemptsForPayments :many +SELECT + ha.id, + ha.attempt_index, + ha.payment_id, + ha.session_key, + ha.attempt_time, + ha.payment_hash, + ha.first_hop_amount_msat, + ha.route_total_time_lock, + ha.route_total_amount, + ha.route_source_key, + hr.resolution_type, + hr.resolution_time, + hr.failure_source_index, + hr.htlc_fail_reason, + hr.failure_msg, + hr.settle_preimage +FROM payment_htlc_attempts ha +LEFT JOIN payment_htlc_attempt_resolutions hr ON hr.attempt_index = ha.attempt_index +WHERE ha.payment_id IN (/*SLICE:payment_ids*/?) +ORDER BY ha.payment_id ASC, ha.attempt_time ASC +` + +type FetchHtlcAttemptsForPaymentsRow struct { + ID int64 + AttemptIndex int64 + PaymentID int64 + SessionKey []byte + AttemptTime time.Time + PaymentHash []byte + FirstHopAmountMsat int64 + RouteTotalTimeLock int32 + RouteTotalAmount int64 + RouteSourceKey []byte + ResolutionType sql.NullInt32 + ResolutionTime sql.NullTime + FailureSourceIndex sql.NullInt32 + HtlcFailReason sql.NullInt32 + FailureMsg []byte + SettlePreimage []byte +} + +func (q *Queries) FetchHtlcAttemptsForPayments(ctx context.Context, paymentIds []int64) ([]FetchHtlcAttemptsForPaymentsRow, error) { + query := fetchHtlcAttemptsForPayments + var queryParams []interface{} + if len(paymentIds) > 0 { + for _, v := range paymentIds { + queryParams = append(queryParams, v) + } + query = strings.Replace(query, "/*SLICE:payment_ids*/?", makeQueryParams(len(queryParams), len(paymentIds)), 1) + } else { + query = strings.Replace(query, "/*SLICE:payment_ids*/?", "NULL", 1) + } + rows, err := q.db.QueryContext(ctx, query, queryParams...) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FetchHtlcAttemptsForPaymentsRow + for rows.Next() { + var i FetchHtlcAttemptsForPaymentsRow + if err := rows.Scan( + &i.ID, + &i.AttemptIndex, + &i.PaymentID, + &i.SessionKey, + &i.AttemptTime, + &i.PaymentHash, + &i.FirstHopAmountMsat, + &i.RouteTotalTimeLock, + &i.RouteTotalAmount, + &i.RouteSourceKey, + &i.ResolutionType, + &i.ResolutionTime, + &i.FailureSourceIndex, + &i.HtlcFailReason, + &i.FailureMsg, + &i.SettlePreimage, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const fetchPayment = `-- name: FetchPayment :one +SELECT + p.id, p.amount_msat, p.created_at, p.payment_identifier, p.fail_reason, + i.intent_type AS "intent_type", + i.intent_payload AS "intent_payload" +FROM payments p +LEFT JOIN payment_intents i ON i.payment_id = p.id +WHERE p.payment_identifier = $1 +` + +type FetchPaymentRow struct { + Payment Payment + IntentType sql.NullInt16 + IntentPayload []byte +} + +func (q *Queries) FetchPayment(ctx context.Context, paymentIdentifier []byte) (FetchPaymentRow, error) { + row := q.db.QueryRowContext(ctx, fetchPayment, paymentIdentifier) + var i FetchPaymentRow + err := row.Scan( + &i.Payment.ID, + &i.Payment.AmountMsat, + &i.Payment.CreatedAt, + &i.Payment.PaymentIdentifier, + &i.Payment.FailReason, + &i.IntentType, + &i.IntentPayload, + ) + return i, err +} + +const fetchPaymentDuplicates = `-- name: FetchPaymentDuplicates :many +SELECT + id, + payment_id, + payment_identifier, + amount_msat, + created_at, + fail_reason, + settle_preimage, + settle_time +FROM payment_duplicates +WHERE payment_id = $1 +ORDER BY id ASC +` + +// Fetch all duplicate payment records from the payment_duplicates table. +func (q *Queries) FetchPaymentDuplicates(ctx context.Context, paymentID int64) ([]PaymentDuplicate, error) { + rows, err := q.db.QueryContext(ctx, fetchPaymentDuplicates, paymentID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []PaymentDuplicate + for rows.Next() { + var i PaymentDuplicate + if err := rows.Scan( + &i.ID, + &i.PaymentID, + &i.PaymentIdentifier, + &i.AmountMsat, + &i.CreatedAt, + &i.FailReason, + &i.SettlePreimage, + &i.SettleTime, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const fetchPaymentLevelFirstHopCustomRecords = `-- name: FetchPaymentLevelFirstHopCustomRecords :many +SELECT + l.id, + l.payment_id, + l.key, + l.value +FROM payment_first_hop_custom_records l +WHERE l.payment_id IN (/*SLICE:payment_ids*/?) +ORDER BY l.payment_id ASC, l.key ASC +` + +func (q *Queries) FetchPaymentLevelFirstHopCustomRecords(ctx context.Context, paymentIds []int64) ([]PaymentFirstHopCustomRecord, error) { + query := fetchPaymentLevelFirstHopCustomRecords + var queryParams []interface{} + if len(paymentIds) > 0 { + for _, v := range paymentIds { + queryParams = append(queryParams, v) + } + query = strings.Replace(query, "/*SLICE:payment_ids*/?", makeQueryParams(len(queryParams), len(paymentIds)), 1) + } else { + query = strings.Replace(query, "/*SLICE:payment_ids*/?", "NULL", 1) + } + rows, err := q.db.QueryContext(ctx, query, queryParams...) + if err != nil { + return nil, err + } + defer rows.Close() + var items []PaymentFirstHopCustomRecord + for rows.Next() { + var i PaymentFirstHopCustomRecord + if err := rows.Scan( + &i.ID, + &i.PaymentID, + &i.Key, + &i.Value, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const fetchPaymentsByIDs = `-- name: FetchPaymentsByIDs :many +SELECT + p.id, + p.amount_msat, + p.created_at, + p.payment_identifier, + p.fail_reason, + pi.intent_type, + pi.intent_payload +FROM payments p +LEFT JOIN payment_intents pi ON pi.payment_id = p.id +WHERE p.id IN (/*SLICE:payment_ids*/?) +ORDER BY p.id ASC +` + +type FetchPaymentsByIDsRow struct { + ID int64 + AmountMsat int64 + CreatedAt time.Time + PaymentIdentifier []byte + FailReason sql.NullInt32 + IntentType sql.NullInt16 + IntentPayload []byte +} + +// Batch fetch payment and intent data for a set of payment IDs. +// Used to avoid fetching redundant payment data when processing multiple +// attempts for the same payment. +func (q *Queries) FetchPaymentsByIDs(ctx context.Context, paymentIds []int64) ([]FetchPaymentsByIDsRow, error) { + query := fetchPaymentsByIDs + var queryParams []interface{} + if len(paymentIds) > 0 { + for _, v := range paymentIds { + queryParams = append(queryParams, v) + } + query = strings.Replace(query, "/*SLICE:payment_ids*/?", makeQueryParams(len(queryParams), len(paymentIds)), 1) + } else { + query = strings.Replace(query, "/*SLICE:payment_ids*/?", "NULL", 1) + } + rows, err := q.db.QueryContext(ctx, query, queryParams...) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FetchPaymentsByIDsRow + for rows.Next() { + var i FetchPaymentsByIDsRow + if err := rows.Scan( + &i.ID, + &i.AmountMsat, + &i.CreatedAt, + &i.PaymentIdentifier, + &i.FailReason, + &i.IntentType, + &i.IntentPayload, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const fetchRouteLevelFirstHopCustomRecords = `-- name: FetchRouteLevelFirstHopCustomRecords :many +SELECT + l.id, + l.htlc_attempt_index, + l.key, + l.value +FROM payment_attempt_first_hop_custom_records l +WHERE l.htlc_attempt_index IN (/*SLICE:htlc_attempt_indices*/?) +ORDER BY l.htlc_attempt_index ASC, l.key ASC +` + +func (q *Queries) FetchRouteLevelFirstHopCustomRecords(ctx context.Context, htlcAttemptIndices []int64) ([]PaymentAttemptFirstHopCustomRecord, error) { + query := fetchRouteLevelFirstHopCustomRecords + var queryParams []interface{} + if len(htlcAttemptIndices) > 0 { + for _, v := range htlcAttemptIndices { + queryParams = append(queryParams, v) + } + query = strings.Replace(query, "/*SLICE:htlc_attempt_indices*/?", makeQueryParams(len(queryParams), len(htlcAttemptIndices)), 1) + } else { + query = strings.Replace(query, "/*SLICE:htlc_attempt_indices*/?", "NULL", 1) + } + rows, err := q.db.QueryContext(ctx, query, queryParams...) + if err != nil { + return nil, err + } + defer rows.Close() + var items []PaymentAttemptFirstHopCustomRecord + for rows.Next() { + var i PaymentAttemptFirstHopCustomRecord + if err := rows.Scan( + &i.ID, + &i.HtlcAttemptIndex, + &i.Key, + &i.Value, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const filterPayments = `-- name: FilterPayments :many +/* ───────────────────────────────────────────── + fetch queries + ───────────────────────────────────────────── +*/ + +SELECT + p.id, p.amount_msat, p.created_at, p.payment_identifier, p.fail_reason, + i.intent_type AS "intent_type", + i.intent_payload AS "intent_payload" +FROM payments p +LEFT JOIN payment_intents i ON i.payment_id = p.id +WHERE ( + p.id > $1 OR + $1 IS NULL +) AND ( + p.id < $2 OR + $2 IS NULL +) AND ( + p.created_at >= $3 OR + $3 IS NULL +) AND ( + p.created_at <= $4 OR + $4 IS NULL +) AND ( + i.intent_type = $5 OR + $5 IS NULL OR i.intent_type IS NULL +) +ORDER BY + CASE WHEN $6 = false OR $6 IS NULL THEN p.id END ASC, + CASE WHEN $6 = true THEN p.id END DESC +LIMIT $7 +` + +type FilterPaymentsParams struct { + IndexOffsetGet sql.NullInt64 + IndexOffsetLet sql.NullInt64 + CreatedAfter sql.NullTime + CreatedBefore sql.NullTime + IntentType sql.NullInt16 + Reverse interface{} + NumLimit int32 +} + +type FilterPaymentsRow struct { + Payment Payment + IntentType sql.NullInt16 + IntentPayload []byte +} + +func (q *Queries) FilterPayments(ctx context.Context, arg FilterPaymentsParams) ([]FilterPaymentsRow, error) { + rows, err := q.db.QueryContext(ctx, filterPayments, + arg.IndexOffsetGet, + arg.IndexOffsetLet, + arg.CreatedAfter, + arg.CreatedBefore, + arg.IntentType, + arg.Reverse, + arg.NumLimit, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FilterPaymentsRow + for rows.Next() { + var i FilterPaymentsRow + if err := rows.Scan( + &i.Payment.ID, + &i.Payment.AmountMsat, + &i.Payment.CreatedAt, + &i.Payment.PaymentIdentifier, + &i.Payment.FailReason, + &i.IntentType, + &i.IntentPayload, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const insertHtlcAttempt = `-- name: InsertHtlcAttempt :one +INSERT INTO payment_htlc_attempts ( + payment_id, + attempt_index, + session_key, + attempt_time, + payment_hash, + first_hop_amount_msat, + route_total_time_lock, + route_total_amount, + route_source_key) +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9) +RETURNING id +` + +type InsertHtlcAttemptParams struct { + PaymentID int64 + AttemptIndex int64 + SessionKey []byte + AttemptTime time.Time + PaymentHash []byte + FirstHopAmountMsat int64 + RouteTotalTimeLock int32 + RouteTotalAmount int64 + RouteSourceKey []byte +} + +func (q *Queries) InsertHtlcAttempt(ctx context.Context, arg InsertHtlcAttemptParams) (int64, error) { + row := q.db.QueryRowContext(ctx, insertHtlcAttempt, + arg.PaymentID, + arg.AttemptIndex, + arg.SessionKey, + arg.AttemptTime, + arg.PaymentHash, + arg.FirstHopAmountMsat, + arg.RouteTotalTimeLock, + arg.RouteTotalAmount, + arg.RouteSourceKey, + ) + var id int64 + err := row.Scan(&id) + return id, err +} + +const insertPayment = `-- name: InsertPayment :one +INSERT INTO payments ( + amount_msat, + created_at, + payment_identifier, + fail_reason) +VALUES ( + $1, + $2, + $3, + NULL +) +RETURNING id +` + +type InsertPaymentParams struct { + AmountMsat int64 + CreatedAt time.Time + PaymentIdentifier []byte +} + +// Insert a new payment and return its ID. +// When creating a payment we don't have a fail reason because we start the +// payment process. +func (q *Queries) InsertPayment(ctx context.Context, arg InsertPaymentParams) (int64, error) { + row := q.db.QueryRowContext(ctx, insertPayment, arg.AmountMsat, arg.CreatedAt, arg.PaymentIdentifier) + var id int64 + err := row.Scan(&id) + return id, err +} + +const insertPaymentAttemptFirstHopCustomRecord = `-- name: InsertPaymentAttemptFirstHopCustomRecord :exec +INSERT INTO payment_attempt_first_hop_custom_records ( + htlc_attempt_index, + key, + value +) +VALUES ( + $1, + $2, + $3 +) +` + +type InsertPaymentAttemptFirstHopCustomRecordParams struct { + HtlcAttemptIndex int64 + Key int64 + Value []byte +} + +func (q *Queries) InsertPaymentAttemptFirstHopCustomRecord(ctx context.Context, arg InsertPaymentAttemptFirstHopCustomRecordParams) error { + _, err := q.db.ExecContext(ctx, insertPaymentAttemptFirstHopCustomRecord, arg.HtlcAttemptIndex, arg.Key, arg.Value) + return err +} + +const insertPaymentDuplicateMig = `-- name: InsertPaymentDuplicateMig :one +INSERT INTO payment_duplicates ( + payment_id, + payment_identifier, + amount_msat, + created_at, + fail_reason, + settle_preimage, + settle_time +) +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7 +) +RETURNING id +` + +type InsertPaymentDuplicateMigParams struct { + PaymentID int64 + PaymentIdentifier []byte + AmountMsat int64 + CreatedAt time.Time + FailReason sql.NullInt32 + SettlePreimage []byte + SettleTime sql.NullTime +} + +// Insert a duplicate payment record into the payment_duplicates table and +// return its ID. +func (q *Queries) InsertPaymentDuplicateMig(ctx context.Context, arg InsertPaymentDuplicateMigParams) (int64, error) { + row := q.db.QueryRowContext(ctx, insertPaymentDuplicateMig, + arg.PaymentID, + arg.PaymentIdentifier, + arg.AmountMsat, + arg.CreatedAt, + arg.FailReason, + arg.SettlePreimage, + arg.SettleTime, + ) + var id int64 + err := row.Scan(&id) + return id, err +} + +const insertPaymentFirstHopCustomRecord = `-- name: InsertPaymentFirstHopCustomRecord :exec +INSERT INTO payment_first_hop_custom_records ( + payment_id, + key, + value +) +VALUES ( + $1, + $2, + $3 +) +` + +type InsertPaymentFirstHopCustomRecordParams struct { + PaymentID int64 + Key int64 + Value []byte +} + +func (q *Queries) InsertPaymentFirstHopCustomRecord(ctx context.Context, arg InsertPaymentFirstHopCustomRecordParams) error { + _, err := q.db.ExecContext(ctx, insertPaymentFirstHopCustomRecord, arg.PaymentID, arg.Key, arg.Value) + return err +} + +const insertPaymentHopCustomRecord = `-- name: InsertPaymentHopCustomRecord :exec +INSERT INTO payment_hop_custom_records ( + hop_id, + key, + value +) +VALUES ( + $1, + $2, + $3 +) +` + +type InsertPaymentHopCustomRecordParams struct { + HopID int64 + Key int64 + Value []byte +} + +func (q *Queries) InsertPaymentHopCustomRecord(ctx context.Context, arg InsertPaymentHopCustomRecordParams) error { + _, err := q.db.ExecContext(ctx, insertPaymentHopCustomRecord, arg.HopID, arg.Key, arg.Value) + return err +} + +const insertPaymentIntent = `-- name: InsertPaymentIntent :one +INSERT INTO payment_intents ( + payment_id, + intent_type, + intent_payload) +VALUES ( + $1, + $2, + $3 +) +RETURNING id +` + +type InsertPaymentIntentParams struct { + PaymentID int64 + IntentType int16 + IntentPayload []byte +} + +// Insert a payment intent for a given payment and return its ID. +func (q *Queries) InsertPaymentIntent(ctx context.Context, arg InsertPaymentIntentParams) (int64, error) { + row := q.db.QueryRowContext(ctx, insertPaymentIntent, arg.PaymentID, arg.IntentType, arg.IntentPayload) + var id int64 + err := row.Scan(&id) + return id, err +} + +const insertPaymentMig = `-- name: InsertPaymentMig :one +/* ───────────────────────────────────────────── + Migration-specific queries + + These queries are used ONLY for the one-time migration from KV to SQL. + They are optimized for bulk historical data import, not runtime usage. + ───────────────────────────────────────────── +*/ + +INSERT INTO payments ( + amount_msat, + created_at, + payment_identifier, + fail_reason) +VALUES ( + $1, + $2, + $3, + $4 +) +RETURNING id +` + +type InsertPaymentMigParams struct { + AmountMsat int64 + CreatedAt time.Time + PaymentIdentifier []byte + FailReason sql.NullInt32 +} + +// Migration-specific payment insert that allows setting fail_reason. +// Normal InsertPayment forces fail_reason to NULL since new payments +// aren't failed yet. During migration, we're inserting historical data +// that may already be failed. +func (q *Queries) InsertPaymentMig(ctx context.Context, arg InsertPaymentMigParams) (int64, error) { + row := q.db.QueryRowContext(ctx, insertPaymentMig, + arg.AmountMsat, + arg.CreatedAt, + arg.PaymentIdentifier, + arg.FailReason, + ) + var id int64 + err := row.Scan(&id) + return id, err +} + +const insertRouteHop = `-- name: InsertRouteHop :one +INSERT INTO payment_route_hops ( + htlc_attempt_index, + hop_index, + pub_key, + scid, + outgoing_time_lock, + amt_to_forward, + meta_data +) +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7 +) +RETURNING id +` + +type InsertRouteHopParams struct { + HtlcAttemptIndex int64 + HopIndex int32 + PubKey []byte + Scid string + OutgoingTimeLock int32 + AmtToForward int64 + MetaData []byte +} + +func (q *Queries) InsertRouteHop(ctx context.Context, arg InsertRouteHopParams) (int64, error) { + row := q.db.QueryRowContext(ctx, insertRouteHop, + arg.HtlcAttemptIndex, + arg.HopIndex, + arg.PubKey, + arg.Scid, + arg.OutgoingTimeLock, + arg.AmtToForward, + arg.MetaData, + ) + var id int64 + err := row.Scan(&id) + return id, err +} + +const insertRouteHopAmp = `-- name: InsertRouteHopAmp :exec +INSERT INTO payment_route_hop_amp ( + hop_id, + root_share, + set_id, + child_index +) +VALUES ( + $1, + $2, + $3, + $4 +) +` + +type InsertRouteHopAmpParams struct { + HopID int64 + RootShare []byte + SetID []byte + ChildIndex int32 +} + +func (q *Queries) InsertRouteHopAmp(ctx context.Context, arg InsertRouteHopAmpParams) error { + _, err := q.db.ExecContext(ctx, insertRouteHopAmp, + arg.HopID, + arg.RootShare, + arg.SetID, + arg.ChildIndex, + ) + return err +} + +const insertRouteHopBlinded = `-- name: InsertRouteHopBlinded :exec +INSERT INTO payment_route_hop_blinded ( + hop_id, + encrypted_data, + blinding_point, + blinded_path_total_amt +) +VALUES ( + $1, + $2, + $3, + $4 +) +` + +type InsertRouteHopBlindedParams struct { + HopID int64 + EncryptedData []byte + BlindingPoint []byte + BlindedPathTotalAmt sql.NullInt64 +} + +func (q *Queries) InsertRouteHopBlinded(ctx context.Context, arg InsertRouteHopBlindedParams) error { + _, err := q.db.ExecContext(ctx, insertRouteHopBlinded, + arg.HopID, + arg.EncryptedData, + arg.BlindingPoint, + arg.BlindedPathTotalAmt, + ) + return err +} + +const insertRouteHopMpp = `-- name: InsertRouteHopMpp :exec +INSERT INTO payment_route_hop_mpp ( + hop_id, + payment_addr, + total_msat +) +VALUES ( + $1, + $2, + $3 +) +` + +type InsertRouteHopMppParams struct { + HopID int64 + PaymentAddr []byte + TotalMsat int64 +} + +func (q *Queries) InsertRouteHopMpp(ctx context.Context, arg InsertRouteHopMppParams) error { + _, err := q.db.ExecContext(ctx, insertRouteHopMpp, arg.HopID, arg.PaymentAddr, arg.TotalMsat) + return err +} + +const settleAttempt = `-- name: SettleAttempt :exec +INSERT INTO payment_htlc_attempt_resolutions ( + attempt_index, + resolution_time, + resolution_type, + settle_preimage +) +VALUES ( + $1, + $2, + $3, + $4 +) +` + +type SettleAttemptParams struct { + AttemptIndex int64 + ResolutionTime time.Time + ResolutionType int32 + SettlePreimage []byte +} + +func (q *Queries) SettleAttempt(ctx context.Context, arg SettleAttemptParams) error { + _, err := q.db.ExecContext(ctx, settleAttempt, + arg.AttemptIndex, + arg.ResolutionTime, + arg.ResolutionType, + arg.SettlePreimage, + ) + return err +} diff --git a/sqldb/sqlc/querier.go b/sqldb/sqlc/querier.go index 7b7b0649596..5ea4f0f0def 100644 --- a/sqldb/sqlc/querier.go +++ b/sqldb/sqlc/querier.go @@ -14,24 +14,53 @@ type Querier interface { AddSourceNode(ctx context.Context, nodeID int64) error AddV1ChannelProof(ctx context.Context, arg AddV1ChannelProofParams) (sql.Result, error) ClearKVInvoiceHashIndex(ctx context.Context) error + CountPayments(ctx context.Context) (int64, error) CountZombieChannels(ctx context.Context, version int16) (int64, error) CreateChannel(ctx context.Context, arg CreateChannelParams) (int64, error) DeleteCanceledInvoices(ctx context.Context) (sql.Result, error) DeleteChannelPolicyExtraTypes(ctx context.Context, channelPolicyID int64) error DeleteChannels(ctx context.Context, ids []int64) error DeleteExtraNodeType(ctx context.Context, arg DeleteExtraNodeTypeParams) error + // Delete all failed HTLC attempts for the given payment. Resolution type 2 + // indicates a failed attempt. + DeleteFailedAttempts(ctx context.Context, paymentID int64) error DeleteInvoice(ctx context.Context, arg DeleteInvoiceParams) (sql.Result, error) DeleteNode(ctx context.Context, id int64) error DeleteNodeAddresses(ctx context.Context, nodeID int64) error DeleteNodeByPubKey(ctx context.Context, arg DeleteNodeByPubKeyParams) (sql.Result, error) DeleteNodeFeature(ctx context.Context, arg DeleteNodeFeatureParams) error + DeletePayment(ctx context.Context, id int64) error DeletePruneLogEntriesInRange(ctx context.Context, arg DeletePruneLogEntriesInRangeParams) error DeleteUnconnectedNodes(ctx context.Context) ([][]byte, error) DeleteZombieChannel(ctx context.Context, arg DeleteZombieChannelParams) (sql.Result, error) + FailAttempt(ctx context.Context, arg FailAttemptParams) error + FailPayment(ctx context.Context, arg FailPaymentParams) (sql.Result, error) FetchAMPSubInvoiceHTLCs(ctx context.Context, arg FetchAMPSubInvoiceHTLCsParams) ([]FetchAMPSubInvoiceHTLCsRow, error) FetchAMPSubInvoices(ctx context.Context, arg FetchAMPSubInvoicesParams) ([]AmpSubInvoice, error) + // Fetch all inflight attempts with their payment data using pagination. + // Returns attempt data joined with payment and intent data to avoid separate queries. + FetchAllInflightAttempts(ctx context.Context, arg FetchAllInflightAttemptsParams) ([]PaymentHtlcAttempt, error) + // Fetch duplicate payment records ordered by payment_id and id. + FetchAllPaymentDuplicates(ctx context.Context, arg FetchAllPaymentDuplicatesParams) ([]PaymentDuplicate, error) + FetchHopLevelCustomRecords(ctx context.Context, hopIds []int64) ([]PaymentHopCustomRecord, error) + FetchHopsForAttempts(ctx context.Context, htlcAttemptIndices []int64) ([]FetchHopsForAttemptsRow, error) + // Batch query to fetch only HTLC resolution status for multiple payments. + // We don't need to order by payment_id and attempt_time because we will + // group the resolutions by payment_id in the background. + FetchHtlcAttemptResolutionsForPayments(ctx context.Context, paymentIds []int64) ([]FetchHtlcAttemptResolutionsForPaymentsRow, error) + FetchHtlcAttemptsForPayments(ctx context.Context, paymentIds []int64) ([]FetchHtlcAttemptsForPaymentsRow, error) + FetchPayment(ctx context.Context, paymentIdentifier []byte) (FetchPaymentRow, error) + // Fetch all duplicate payment records from the payment_duplicates table. + FetchPaymentDuplicates(ctx context.Context, paymentID int64) ([]PaymentDuplicate, error) + FetchPaymentLevelFirstHopCustomRecords(ctx context.Context, paymentIds []int64) ([]PaymentFirstHopCustomRecord, error) + // Batch fetch payment and intent data for a set of payment IDs. + // Used to avoid fetching redundant payment data when processing multiple + // attempts for the same payment. + FetchPaymentsByIDs(ctx context.Context, paymentIds []int64) ([]FetchPaymentsByIDsRow, error) + FetchRouteLevelFirstHopCustomRecords(ctx context.Context, htlcAttemptIndices []int64) ([]PaymentAttemptFirstHopCustomRecord, error) FetchSettledAMPSubInvoices(ctx context.Context, arg FetchSettledAMPSubInvoicesParams) ([]FetchSettledAMPSubInvoicesRow, error) FilterInvoices(ctx context.Context, arg FilterInvoicesParams) ([]Invoice, error) + FilterPayments(ctx context.Context, arg FilterPaymentsParams) ([]FilterPaymentsRow, error) GetAMPInvoiceID(ctx context.Context, setID []byte) (int64, error) GetChannelAndNodesBySCID(ctx context.Context, arg GetChannelAndNodesBySCIDParams) (GetChannelAndNodesBySCIDRow, error) GetChannelByOutpointWithPolicies(ctx context.Context, arg GetChannelByOutpointWithPoliciesParams) (GetChannelByOutpointWithPoliciesRow, error) @@ -101,6 +130,7 @@ type Querier interface { // UpsertEdgePolicy query is used because of the constraint in that query that // requires a policy update to have a newer last_update than the existing one). InsertEdgePolicyMig(ctx context.Context, arg InsertEdgePolicyMigParams) (int64, error) + InsertHtlcAttempt(ctx context.Context, arg InsertHtlcAttemptParams) (int64, error) InsertInvoice(ctx context.Context, arg InsertInvoiceParams) (int64, error) InsertInvoiceFeature(ctx context.Context, arg InsertInvoiceFeatureParams) error InsertInvoiceHTLC(ctx context.Context, arg InsertInvoiceHTLCParams) (int64, error) @@ -114,6 +144,27 @@ type Querier interface { // is used because of the constraint in that query that requires a node update // to have a newer last_update than the existing node). InsertNodeMig(ctx context.Context, arg InsertNodeMigParams) (int64, error) + // Insert a new payment and return its ID. + // When creating a payment we don't have a fail reason because we start the + // payment process. + InsertPayment(ctx context.Context, arg InsertPaymentParams) (int64, error) + InsertPaymentAttemptFirstHopCustomRecord(ctx context.Context, arg InsertPaymentAttemptFirstHopCustomRecordParams) error + // Insert a duplicate payment record into the payment_duplicates table and + // return its ID. + InsertPaymentDuplicateMig(ctx context.Context, arg InsertPaymentDuplicateMigParams) (int64, error) + InsertPaymentFirstHopCustomRecord(ctx context.Context, arg InsertPaymentFirstHopCustomRecordParams) error + InsertPaymentHopCustomRecord(ctx context.Context, arg InsertPaymentHopCustomRecordParams) error + // Insert a payment intent for a given payment and return its ID. + InsertPaymentIntent(ctx context.Context, arg InsertPaymentIntentParams) (int64, error) + // Migration-specific payment insert that allows setting fail_reason. + // Normal InsertPayment forces fail_reason to NULL since new payments + // aren't failed yet. During migration, we're inserting historical data + // that may already be failed. + InsertPaymentMig(ctx context.Context, arg InsertPaymentMigParams) (int64, error) + InsertRouteHop(ctx context.Context, arg InsertRouteHopParams) (int64, error) + InsertRouteHopAmp(ctx context.Context, arg InsertRouteHopAmpParams) error + InsertRouteHopBlinded(ctx context.Context, arg InsertRouteHopBlindedParams) error + InsertRouteHopMpp(ctx context.Context, arg InsertRouteHopMppParams) error IsClosedChannel(ctx context.Context, scid []byte) (bool, error) IsPublicV1Node(ctx context.Context, pubKey []byte) (bool, error) IsZombieChannel(ctx context.Context, arg IsZombieChannelParams) (bool, error) @@ -133,6 +184,7 @@ type Querier interface { OnInvoiceSettled(ctx context.Context, arg OnInvoiceSettledParams) error SetKVInvoicePaymentHash(ctx context.Context, arg SetKVInvoicePaymentHashParams) error SetMigration(ctx context.Context, arg SetMigrationParams) error + SettleAttempt(ctx context.Context, arg SettleAttemptParams) error UpdateAMPSubInvoiceHTLCPreimage(ctx context.Context, arg UpdateAMPSubInvoiceHTLCPreimageParams) (sql.Result, error) UpdateAMPSubInvoiceState(ctx context.Context, arg UpdateAMPSubInvoiceStateParams) error UpdateInvoiceAmountPaid(ctx context.Context, arg UpdateInvoiceAmountPaidParams) (sql.Result, error) diff --git a/sqldb/sqlc/queries/payments.sql b/sqldb/sqlc/queries/payments.sql new file mode 100644 index 00000000000..e8f57c21c4f --- /dev/null +++ b/sqldb/sqlc/queries/payments.sql @@ -0,0 +1,453 @@ +/* ───────────────────────────────────────────── + fetch queries + ───────────────────────────────────────────── +*/ + +-- name: FilterPayments :many +SELECT + sqlc.embed(p), + i.intent_type AS "intent_type", + i.intent_payload AS "intent_payload" +FROM payments p +LEFT JOIN payment_intents i ON i.payment_id = p.id +WHERE ( + p.id > sqlc.narg('index_offset_get') OR + sqlc.narg('index_offset_get') IS NULL +) AND ( + p.id < sqlc.narg('index_offset_let') OR + sqlc.narg('index_offset_let') IS NULL +) AND ( + p.created_at >= sqlc.narg('created_after') OR + sqlc.narg('created_after') IS NULL +) AND ( + p.created_at <= sqlc.narg('created_before') OR + sqlc.narg('created_before') IS NULL +) AND ( + i.intent_type = sqlc.narg('intent_type') OR + sqlc.narg('intent_type') IS NULL OR i.intent_type IS NULL +) +ORDER BY + CASE WHEN sqlc.narg('reverse') = false OR sqlc.narg('reverse') IS NULL THEN p.id END ASC, + CASE WHEN sqlc.narg('reverse') = true THEN p.id END DESC +LIMIT @num_limit; + +-- name: FetchPayment :one +SELECT + sqlc.embed(p), + i.intent_type AS "intent_type", + i.intent_payload AS "intent_payload" +FROM payments p +LEFT JOIN payment_intents i ON i.payment_id = p.id +WHERE p.payment_identifier = $1; + +-- name: FetchPaymentDuplicates :many +-- Fetch all duplicate payment records from the payment_duplicates table. +SELECT + id, + payment_id, + payment_identifier, + amount_msat, + created_at, + fail_reason, + settle_preimage, + settle_time +FROM payment_duplicates +WHERE payment_id = $1 +ORDER BY id ASC; + +-- name: FetchAllPaymentDuplicates :many +-- Fetch duplicate payment records ordered by payment_id and id. +SELECT + id, + payment_id, + payment_identifier, + amount_msat, + created_at, + fail_reason, + settle_preimage, + settle_time +FROM payment_duplicates +WHERE ( + payment_id > @after_payment_id OR + (payment_id = @after_payment_id AND id > @after_id) +) +ORDER BY payment_id ASC, id ASC +LIMIT @num_limit; + +-- name: CountPayments :one +SELECT COUNT(*) FROM payments; + +-- name: FetchHtlcAttemptsForPayments :many +SELECT + ha.id, + ha.attempt_index, + ha.payment_id, + ha.session_key, + ha.attempt_time, + ha.payment_hash, + ha.first_hop_amount_msat, + ha.route_total_time_lock, + ha.route_total_amount, + ha.route_source_key, + hr.resolution_type, + hr.resolution_time, + hr.failure_source_index, + hr.htlc_fail_reason, + hr.failure_msg, + hr.settle_preimage +FROM payment_htlc_attempts ha +LEFT JOIN payment_htlc_attempt_resolutions hr ON hr.attempt_index = ha.attempt_index +WHERE ha.payment_id IN (sqlc.slice('payment_ids')/*SLICE:payment_ids*/) +ORDER BY ha.payment_id ASC, ha.attempt_time ASC; + +-- name: FetchHtlcAttemptResolutionsForPayments :many +-- Batch query to fetch only HTLC resolution status for multiple payments. +-- We don't need to order by payment_id and attempt_time because we will +-- group the resolutions by payment_id in the background. +SELECT + ha.payment_id, + hr.resolution_type +FROM payment_htlc_attempts ha +LEFT JOIN payment_htlc_attempt_resolutions hr ON hr.attempt_index = ha.attempt_index +WHERE ha.payment_id IN (sqlc.slice('payment_ids')/*SLICE:payment_ids*/); + +-- name: FetchPaymentsByIDs :many +-- Batch fetch payment and intent data for a set of payment IDs. +-- Used to avoid fetching redundant payment data when processing multiple +-- attempts for the same payment. +SELECT + p.id, + p.amount_msat, + p.created_at, + p.payment_identifier, + p.fail_reason, + pi.intent_type, + pi.intent_payload +FROM payments p +LEFT JOIN payment_intents pi ON pi.payment_id = p.id +WHERE p.id IN (sqlc.slice('payment_ids')/*SLICE:payment_ids*/) +ORDER BY p.id ASC; + +-- name: FetchAllInflightAttempts :many +-- Fetch all inflight attempts with their payment data using pagination. +-- Returns attempt data joined with payment and intent data to avoid separate queries. +SELECT + ha.id, + ha.attempt_index, + ha.payment_id, + ha.session_key, + ha.attempt_time, + ha.payment_hash, + ha.first_hop_amount_msat, + ha.route_total_time_lock, + ha.route_total_amount, + ha.route_source_key +FROM payment_htlc_attempts ha +WHERE NOT EXISTS ( + SELECT 1 FROM payment_htlc_attempt_resolutions hr + WHERE hr.attempt_index = ha.attempt_index +) +AND ha.attempt_index > $1 +ORDER BY ha.attempt_index ASC +LIMIT $2; + +-- name: FetchHopsForAttempts :many +SELECT + h.id, + h.htlc_attempt_index, + h.hop_index, + h.pub_key, + h.scid, + h.outgoing_time_lock, + h.amt_to_forward, + h.meta_data, + m.payment_addr AS mpp_payment_addr, + m.total_msat AS mpp_total_msat, + a.root_share AS amp_root_share, + a.set_id AS amp_set_id, + a.child_index AS amp_child_index, + b.encrypted_data, + b.blinding_point, + b.blinded_path_total_amt +FROM payment_route_hops h +LEFT JOIN payment_route_hop_mpp m ON m.hop_id = h.id +LEFT JOIN payment_route_hop_amp a ON a.hop_id = h.id +LEFT JOIN payment_route_hop_blinded b ON b.hop_id = h.id +WHERE h.htlc_attempt_index IN (sqlc.slice('htlc_attempt_indices')/*SLICE:htlc_attempt_indices*/) +ORDER BY h.htlc_attempt_index ASC, h.hop_index ASC; + + +-- name: FetchPaymentLevelFirstHopCustomRecords :many +SELECT + l.id, + l.payment_id, + l.key, + l.value +FROM payment_first_hop_custom_records l +WHERE l.payment_id IN (sqlc.slice('payment_ids')/*SLICE:payment_ids*/) +ORDER BY l.payment_id ASC, l.key ASC; + +-- name: FetchRouteLevelFirstHopCustomRecords :many +SELECT + l.id, + l.htlc_attempt_index, + l.key, + l.value +FROM payment_attempt_first_hop_custom_records l +WHERE l.htlc_attempt_index IN (sqlc.slice('htlc_attempt_indices')/*SLICE:htlc_attempt_indices*/) +ORDER BY l.htlc_attempt_index ASC, l.key ASC; + +-- name: FetchHopLevelCustomRecords :many +SELECT + l.id, + l.hop_id, + l.key, + l.value +FROM payment_hop_custom_records l +WHERE l.hop_id IN (sqlc.slice('hop_ids')/*SLICE:hop_ids*/) +ORDER BY l.hop_id ASC, l.key ASC; + + +-- name: DeletePayment :exec +DELETE FROM payments WHERE id = $1; + +-- name: DeleteFailedAttempts :exec +-- Delete all failed HTLC attempts for the given payment. Resolution type 2 +-- indicates a failed attempt. +DELETE FROM payment_htlc_attempts WHERE payment_id = $1 AND attempt_index IN ( + SELECT attempt_index FROM payment_htlc_attempt_resolutions WHERE resolution_type = 2 +); + +-- name: InsertPaymentIntent :one +-- Insert a payment intent for a given payment and return its ID. +INSERT INTO payment_intents ( + payment_id, + intent_type, + intent_payload) +VALUES ( + @payment_id, + @intent_type, + @intent_payload +) +RETURNING id; + +-- name: InsertPayment :one +-- Insert a new payment and return its ID. +-- When creating a payment we don't have a fail reason because we start the +-- payment process. +INSERT INTO payments ( + amount_msat, + created_at, + payment_identifier, + fail_reason) +VALUES ( + @amount_msat, + @created_at, + @payment_identifier, + NULL +) +RETURNING id; + +-- name: InsertPaymentFirstHopCustomRecord :exec +INSERT INTO payment_first_hop_custom_records ( + payment_id, + key, + value +) +VALUES ( + @payment_id, + @key, + @value +); + +-- name: InsertHtlcAttempt :one +INSERT INTO payment_htlc_attempts ( + payment_id, + attempt_index, + session_key, + attempt_time, + payment_hash, + first_hop_amount_msat, + route_total_time_lock, + route_total_amount, + route_source_key) +VALUES ( + @payment_id, + @attempt_index, + @session_key, + @attempt_time, + @payment_hash, + @first_hop_amount_msat, + @route_total_time_lock, + @route_total_amount, + @route_source_key) +RETURNING id; + +-- name: InsertPaymentAttemptFirstHopCustomRecord :exec +INSERT INTO payment_attempt_first_hop_custom_records ( + htlc_attempt_index, + key, + value +) +VALUES ( + @htlc_attempt_index, + @key, + @value +); + +-- name: InsertRouteHop :one +INSERT INTO payment_route_hops ( + htlc_attempt_index, + hop_index, + pub_key, + scid, + outgoing_time_lock, + amt_to_forward, + meta_data +) +VALUES ( + @htlc_attempt_index, + @hop_index, + @pub_key, + @scid, + @outgoing_time_lock, + @amt_to_forward, + @meta_data +) +RETURNING id; + +-- name: InsertRouteHopMpp :exec +INSERT INTO payment_route_hop_mpp ( + hop_id, + payment_addr, + total_msat +) +VALUES ( + @hop_id, + @payment_addr, + @total_msat +); + +-- name: InsertRouteHopAmp :exec +INSERT INTO payment_route_hop_amp ( + hop_id, + root_share, + set_id, + child_index +) +VALUES ( + @hop_id, + @root_share, + @set_id, + @child_index +); + +-- name: InsertRouteHopBlinded :exec +INSERT INTO payment_route_hop_blinded ( + hop_id, + encrypted_data, + blinding_point, + blinded_path_total_amt +) +VALUES ( + @hop_id, + @encrypted_data, + @blinding_point, + @blinded_path_total_amt +); + +-- name: InsertPaymentHopCustomRecord :exec +INSERT INTO payment_hop_custom_records ( + hop_id, + key, + value +) +VALUES ( + @hop_id, + @key, + @value +); + +-- name: SettleAttempt :exec +INSERT INTO payment_htlc_attempt_resolutions ( + attempt_index, + resolution_time, + resolution_type, + settle_preimage +) +VALUES ( + @attempt_index, + @resolution_time, + @resolution_type, + @settle_preimage +); + +-- name: FailAttempt :exec +INSERT INTO payment_htlc_attempt_resolutions ( + attempt_index, + resolution_time, + resolution_type, + failure_source_index, + htlc_fail_reason, + failure_msg +) +VALUES ( + @attempt_index, + @resolution_time, + @resolution_type, + @failure_source_index, + @htlc_fail_reason, + @failure_msg +); + +-- name: FailPayment :execresult +UPDATE payments SET fail_reason = $1 WHERE payment_identifier = $2; + +/* ───────────────────────────────────────────── + Migration-specific queries + + These queries are used ONLY for the one-time migration from KV to SQL. + They are optimized for bulk historical data import, not runtime usage. + ───────────────────────────────────────────── +*/ + +-- name: InsertPaymentMig :one +-- Migration-specific payment insert that allows setting fail_reason. +-- Normal InsertPayment forces fail_reason to NULL since new payments +-- aren't failed yet. During migration, we're inserting historical data +-- that may already be failed. +INSERT INTO payments ( + amount_msat, + created_at, + payment_identifier, + fail_reason) +VALUES ( + @amount_msat, + @created_at, + @payment_identifier, + @fail_reason +) +RETURNING id; + +-- name: InsertPaymentDuplicateMig :one +-- Insert a duplicate payment record into the payment_duplicates table and +-- return its ID. +INSERT INTO payment_duplicates ( + payment_id, + payment_identifier, + amount_msat, + created_at, + fail_reason, + settle_preimage, + settle_time +) +VALUES ( + @payment_id, + @payment_identifier, + @amount_msat, + @created_at, + @fail_reason, + @settle_preimage, + @settle_time +) +RETURNING id;