diff --git a/channeldb/forwarding_log.go b/channeldb/forwarding_log.go index a80985a01be..f5e7770bbe3 100644 --- a/channeldb/forwarding_log.go +++ b/channeldb/forwarding_log.go @@ -416,6 +416,150 @@ func (f *ForwardingLog) Query(q ForwardingEventQuery) (ForwardingLogTimeSlice, return resp, nil } +// DeleteStats contains statistics about a forwarding history deletion +// operation. +type DeleteStats struct { + // NumEventsDeleted is the total number of forwarding events that were + // deleted from the database. + NumEventsDeleted uint64 + + // TotalFeeMsat is the sum of all fees (AmtIn - AmtOut) from the + // deleted events, expressed in millisatoshis. + TotalFeeMsat int64 +} + +// DeleteForwardingEvents deletes all forwarding events older than the specified +// endTime from the database. The deletion is performed in batches to avoid +// holding large database transactions. This method returns statistics about the +// deletion including the number of events deleted and the total fees earned +// from those events. +// +// The batchSize parameter controls how many events are deleted per database +// transaction. If batchSize is 0, a default of 10000 is used. The maximum +// allowed batch size is MaxResponseEvents (50000) to prevent resource exhaustion. +func (f *ForwardingLog) DeleteForwardingEvents(endTime time.Time, + batchSize int) (DeleteStats, error) { + + // Set default batch size if not specified, and enforce maximum. + if batchSize <= 0 { + batchSize = 10000 + } + if batchSize > MaxResponseEvents { + batchSize = MaxResponseEvents + } + + var stats DeleteStats + + // We'll continue deleting batches until there are no more events to + // delete. + for { + var ( + batchDeleted int + batchFees int64 + ) + + err := kvdb.Update(f.db, func(tx kvdb.RwTx) error { + // Fetch the forwarding log bucket. If it doesn't exist, + // there's nothing to delete. + logBucket := tx.ReadWriteBucket(forwardingLogBucket) + if logBucket == nil { + return ErrNoForwardingEvents + } + + // We'll use a cursor to iterate through events in time + // order. + cursor := logBucket.ReadWriteCursor() + + // Next, encode the end time as our upper bound for + // deletion. + var endTimeBytes [8]byte + byteOrder.PutUint64( + endTimeBytes[:], uint64(endTime.UnixNano()), + ) + + // Collect keys to delete in this batch. We can't delete + // while iterating as it may corrupt the cursor. + keysToDelete := make([][]byte, 0, batchSize) + + // Seek to the beginning and iterate through events + // until we reach the end time or batch limit. + // + //nolint:lll + for timestamp, eventBytes := cursor.First(); timestamp != nil; timestamp, eventBytes = cursor.Next() { + // Stop if we've reached or passed the end time. + if bytes.Compare( + timestamp, endTimeBytes[:], + ) > 0 { + break + } + + // Stop if we've reached the batch size limit. + if len(keysToDelete) >= batchSize { + break + } + + // Decode the event, as we need to parse it to + // obtain the fee. + readBuf := bytes.NewReader(eventBytes) + if readBuf.Len() > 0 { + var event ForwardingEvent + err := decodeForwardingEvent( + readBuf, &event, + ) + if err != nil { + return err + } + + // Calculate the fee for this event. Fee + // is the difference between incoming + // and outgoing amounts. + fee := int64(event.AmtIn - event.AmtOut) + batchFees += fee + } + + // Make a copy of the key to delete later. + keyCopy := make([]byte, len(timestamp)) + copy(keyCopy, timestamp) + keysToDelete = append(keysToDelete, keyCopy) + } + + // Now delete all the collected keys. + for _, key := range keysToDelete { + if err := logBucket.Delete(key); err != nil { + return err + } + } + + batchDeleted = len(keysToDelete) + return nil + }, func() { + batchDeleted = 0 + batchFees = 0 + }) + + if err != nil { + // If the bucket doesn't exist, we're done. + if err == ErrNoForwardingEvents { + break + } + return stats, err + } + + // Update our running statistics. + stats.NumEventsDeleted += uint64(batchDeleted) + stats.TotalFeeMsat += batchFees + + // If we deleted fewer events than the batch size, we're done. + if batchDeleted < batchSize { + break + } + + // Otherwise, continue with the next batch. + } + + return stats, nil +} + // makeUniqueTimestamps takes a slice of forwarding events, sorts it by the // event timestamps and then makes sure there are no duplicates in the // timestamps. If duplicates are found, some of the timestamps are increased on diff --git a/channeldb/forwarding_log_test.go b/channeldb/forwarding_log_test.go index 0f589f88a32..cf22c7dc1c8 100644 --- a/channeldb/forwarding_log_test.go +++ b/channeldb/forwarding_log_test.go @@ -13,6 +13,7 @@ import ( "github.com/lightningnetwork/lnd/lnwire" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "pgregory.net/rapid" ) // TestForwardingLogBasicStorageAndQuery tests that we're able to store and @@ -606,3 +607,526 @@ func TestForwardingLogQueryChanIDs(t *testing.T) { }) } } + +// TestForwardingLogDeletion tests the basic deletion functionality of the +// forwarding log. +func TestForwardingLogDeletion(t *testing.T) { + t.Parallel() + + db, err := MakeTestDB(t) + require.NoError(t, err, "unable to make test db") + + log := ForwardingLog{ + db: db, + } + + // Create 50 events spanning 500 minutes (10 min intervals). + initialTime := time.Unix(1000, 0) + timestamp := initialTime + numEvents := 50 + events := make([]ForwardingEvent, numEvents) + + var expectedTotalFees int64 + for i := 0; i < numEvents; i++ { + amtIn := lnwire.MilliSatoshi(10000 + rand.Intn(5000)) + amtOut := lnwire.MilliSatoshi(9000 + rand.Intn(4000)) + events[i] = ForwardingEvent{ + Timestamp: timestamp, + IncomingChanID: lnwire.NewShortChanIDFromInt(uint64(i)), + OutgoingChanID: lnwire.NewShortChanIDFromInt(uint64(i + 100)), + AmtIn: amtIn, + AmtOut: amtOut, + IncomingHtlcID: fn.Some(uint64(i)), + OutgoingHtlcID: fn.Some(uint64(i)), + } + expectedTotalFees += int64(amtIn - amtOut) + timestamp = timestamp.Add(time.Minute * 10) + } + + // Add all events to the database. + err = log.AddForwardingEvents(events) + require.NoError(t, err, "unable to add events") + + // Delete all events (use timestamp after the last event). + deleteTime := timestamp.Add(time.Minute) + stats, err := log.DeleteForwardingEvents(deleteTime, 0) + require.NoError(t, err, "unable to delete events") + + // Verify statistics. + assert.Equal(t, uint64(numEvents), stats.NumEventsDeleted, + "wrong number of events deleted") + assert.Equal(t, expectedTotalFees, stats.TotalFeeMsat, + "wrong total fees") + + // Verify all events were deleted by querying. + query := ForwardingEventQuery{ + StartTime: initialTime, + EndTime: timestamp, + NumMaxEvents: 1000, + } + result, err := log.Query(query) + require.NoError(t, err, "query failed") + assert.Empty(t, result.ForwardingEvents, "events should be deleted") +} + +// TestForwardingLogPartialDeletion tests that we can delete a subset of events +// based on time. +func TestForwardingLogPartialDeletion(t *testing.T) { + t.Parallel() + + db, err := MakeTestDB(t) + require.NoError(t, err, "unable to make test db") + + log := ForwardingLog{ + db: db, + } + + initialTime := time.Unix(2000, 0) + timestamp := initialTime + numEvents := 100 + events := make([]ForwardingEvent, numEvents) + + for i := 0; i < numEvents; i++ { + events[i] = ForwardingEvent{ + Timestamp: timestamp, + IncomingChanID: lnwire.NewShortChanIDFromInt(uint64(i)), + OutgoingChanID: lnwire.NewShortChanIDFromInt( + uint64(i + 100), + ), + AmtIn: lnwire.MilliSatoshi(10000), + AmtOut: lnwire.MilliSatoshi(9500), + IncomingHtlcID: fn.Some(uint64(i)), + OutgoingHtlcID: fn.Some(uint64(i)), + } + timestamp = timestamp.Add(time.Minute * 10) + } + + err = log.AddForwardingEvents(events) + require.NoError(t, err, "unable to add events") + + // Delete only the first 50 events (events 0-49). The 50th event is at + // initialTime + 50*10 minutes. + deleteTime := events[49].Timestamp.Add(time.Nanosecond) + stats, err := log.DeleteForwardingEvents(deleteTime, 0) + require.NoError(t, err, "unable to delete events") + + // Should have deleted exactly 50 events. + assert.Equal( + t, uint64(50), stats.NumEventsDeleted, + "wrong number of events deleted", + ) + + // Fee per event is 500 msat, so total should be 50 * 500 = 25000. + assert.Equal(t, int64(25000), stats.TotalFeeMsat, "wrong total fees") + + // Query to verify remaining events (should be 50 events left). + query := ForwardingEventQuery{ + StartTime: initialTime, + EndTime: timestamp, + NumMaxEvents: 1000, + } + result, err := log.Query(query) + require.NoError(t, err, "query failed") + assert.Len( + t, result.ForwardingEvents, 50, + "wrong number of remaining events", + ) + + // The remaining events should be events[50:]. + assert.Equal( + t, events[50:], result.ForwardingEvents, + "wrong events remaining", + ) +} + +// TestForwardingLogBatchDeletion tests that deletion works correctly with +// different batch sizes. +func TestForwardingLogBatchDeletion(t *testing.T) { + t.Parallel() + + db, err := MakeTestDB(t) + require.NoError(t, err, "unable to make test db") + + log := ForwardingLog{ + db: db, + } + + initialTime := time.Unix(3000, 0) + timestamp := initialTime + numEvents := 250 + events := make([]ForwardingEvent, numEvents) + + for i := 0; i < numEvents; i++ { + events[i] = ForwardingEvent{ + Timestamp: timestamp, + IncomingChanID: lnwire.NewShortChanIDFromInt(uint64(i)), + OutgoingChanID: lnwire.NewShortChanIDFromInt(uint64(i + 100)), + AmtIn: lnwire.MilliSatoshi(10000), + AmtOut: lnwire.MilliSatoshi(9000), + IncomingHtlcID: fn.Some(uint64(i)), + OutgoingHtlcID: fn.Some(uint64(i)), + } + timestamp = timestamp.Add(time.Minute) + } + + err = log.AddForwardingEvents(events) + require.NoError(t, err, "unable to add events") + + // Delete with a small batch size to test multiple batches. + deleteTime := timestamp.Add(time.Minute) + stats, err := log.DeleteForwardingEvents(deleteTime, 75) + require.NoError(t, err, "unable to delete events") + + // Should have deleted all events across multiple batches. + assert.Equal(t, uint64(numEvents), stats.NumEventsDeleted, + "wrong number of events deleted") + + // Fee per event is 1000 msat. + expectedFees := int64(numEvents * 1000) + assert.Equal(t, expectedFees, stats.TotalFeeMsat, "wrong total fees") + + // Verify all deleted. + query := ForwardingEventQuery{ + StartTime: initialTime, + EndTime: timestamp, + NumMaxEvents: 1000, + } + result, err := log.Query(query) + require.NoError(t, err, "query failed") + assert.Empty(t, result.ForwardingEvents, "events should be deleted") +} + +// TestForwardingLogDeleteEmpty tests deletion on an empty database. +func TestForwardingLogDeleteEmpty(t *testing.T) { + t.Parallel() + + db, err := MakeTestDB(t) + require.NoError(t, err, "unable to make test db") + + log := ForwardingLog{ + db: db, + } + + // Try to delete from empty database. + deleteTime := time.Now() + stats, err := log.DeleteForwardingEvents(deleteTime, 0) + require.NoError(t, err, "delete should not error on empty db") + + // Should have deleted 0 events with 0 fees. + assert.Equal( + t, uint64(0), stats.NumEventsDeleted, + "should delete 0 events", + ) + assert.Equal( + t, int64(0), stats.TotalFeeMsat, "should have 0 fees", + ) +} + +// TestForwardingLogDeleteTimeBoundary tests deletion at exact time boundaries. +func TestForwardingLogDeleteTimeBoundary(t *testing.T) { + t.Parallel() + + db, err := MakeTestDB(t) + require.NoError(t, err, "unable to make test db") + + log := ForwardingLog{ + db: db, + } + + baseTime := time.Unix(5000, 0) + events := []ForwardingEvent{ + { + Timestamp: baseTime, + IncomingChanID: lnwire.NewShortChanIDFromInt(1), + OutgoingChanID: lnwire.NewShortChanIDFromInt(101), + AmtIn: 10000, + AmtOut: 9000, + IncomingHtlcID: fn.Some(uint64(0)), + OutgoingHtlcID: fn.Some(uint64(0)), + }, + { + Timestamp: baseTime.Add(time.Hour), + IncomingChanID: lnwire.NewShortChanIDFromInt(2), + OutgoingChanID: lnwire.NewShortChanIDFromInt(102), + AmtIn: 10000, + AmtOut: 9000, + IncomingHtlcID: fn.Some(uint64(1)), + OutgoingHtlcID: fn.Some(uint64(1)), + }, + { + Timestamp: baseTime.Add(2 * time.Hour), + IncomingChanID: lnwire.NewShortChanIDFromInt(3), + OutgoingChanID: lnwire.NewShortChanIDFromInt(103), + AmtIn: 10000, + AmtOut: 9000, + IncomingHtlcID: fn.Some(uint64(2)), + OutgoingHtlcID: fn.Some(uint64(2)), + }, + } + + err = log.AddForwardingEvents(events) + require.NoError(t, err, "unable to add events") + + // Delete events at exactly the second event's timestamp. This should + // delete events at baseTime and baseTime+1h. + deleteTime := baseTime.Add(time.Hour) + stats, err := log.DeleteForwardingEvents(deleteTime, 0) + require.NoError(t, err, "unable to delete events") + + // Should delete exactly 2 events (those at or before deleteTime). + assert.Equal( + t, uint64(2), stats.NumEventsDeleted, + "wrong number of events deleted", + ) + + query := ForwardingEventQuery{ + StartTime: baseTime, + EndTime: baseTime.Add(3 * time.Hour), + NumMaxEvents: 10, + } + result, err := log.Query(query) + require.NoError(t, err, "query failed") + + // We should have 1 event remaining. + assert.Len( + t, result.ForwardingEvents, 1, "wrong number remaining", + ) + assert.Equal( + t, events[2], result.ForwardingEvents[0], + "wrong event remaining", + ) +} + +// TestForwardingLogDeleteMaxBatchSize tests that the max batch size is +// enforced. +func TestForwardingLogDeleteMaxBatchSize(t *testing.T) { + t.Parallel() + + db, err := MakeTestDB(t) + require.NoError(t, err, "unable to make test db") + + log := ForwardingLog{ + db: db, + } + + // Create some events. + initialTime := time.Unix(6000, 0) + events := []ForwardingEvent{ + { + Timestamp: initialTime, + IncomingChanID: lnwire.NewShortChanIDFromInt(1), + OutgoingChanID: lnwire.NewShortChanIDFromInt(101), + AmtIn: 10000, + AmtOut: 9000, + IncomingHtlcID: fn.Some(uint64(0)), + OutgoingHtlcID: fn.Some(uint64(0)), + }, + } + + err = log.AddForwardingEvents(events) + require.NoError(t, err, "unable to add events") + + // Try to delete with a batch size larger than MaxResponseEvents. + deleteTime := initialTime.Add(time.Hour) + stats, err := log.DeleteForwardingEvents( + deleteTime, MaxResponseEvents+1000, + ) + require.NoError(t, err, "delete should succeed") + + // Should have deleted the event (batch size should be capped). + assert.Equal( + t, uint64(1), stats.NumEventsDeleted, "event should be deleted", + ) +} + +// TestForwardingLogDeleteIdempotent tests that deletion is idempotent. +func TestForwardingLogDeleteIdempotent(t *testing.T) { + t.Parallel() + + db, err := MakeTestDB(t) + require.NoError(t, err, "unable to make test db") + + log := ForwardingLog{ + db: db, + } + + // Create events. + initialTime := time.Unix(7000, 0) + timestamp := initialTime + events := make([]ForwardingEvent, 10) + for i := 0; i < 10; i++ { + events[i] = ForwardingEvent{ + Timestamp: timestamp, + IncomingChanID: lnwire.NewShortChanIDFromInt(uint64(i)), + OutgoingChanID: lnwire.NewShortChanIDFromInt(uint64(i + 100)), + AmtIn: lnwire.MilliSatoshi(10000), + AmtOut: lnwire.MilliSatoshi(9000), + IncomingHtlcID: fn.Some(uint64(i)), + OutgoingHtlcID: fn.Some(uint64(i)), + } + timestamp = timestamp.Add(time.Minute) + } + + err = log.AddForwardingEvents(events) + require.NoError(t, err, "unable to add events") + + deleteTime := timestamp + stats1, err := log.DeleteForwardingEvents(deleteTime, 0) + require.NoError(t, err, "first delete failed") + assert.Equal(t, uint64(10), stats1.NumEventsDeleted) + + // Delete again with same time - should delete 0 events. + stats2, err := log.DeleteForwardingEvents(deleteTime, 0) + require.NoError(t, err, "second delete failed") + assert.Equal( + t, uint64(0), stats2.NumEventsDeleted, "should be idempotent", + ) + assert.Equal(t, int64(0), stats2.TotalFeeMsat, "should have no fees") +} + +// TestForwardingLogDeleteInvariants uses property-based testing to verify key +// invariants of the deletion logic. +func TestForwardingLogDeleteInvariants(t *testing.T) { + rapid.Check(t, func(rt *rapid.T) { + db, err := MakeTestDB(t) + if err != nil { + rt.Fatalf("unable to make test db: %v", err) + } + + log := ForwardingLog{ + db: db, + } + + // Generate a random set of events. + baseTime := time.Unix( + rapid.Int64Range(10000, 100000).Draw(rt, "base_time"), + 0, + ) + numEvents := rapid.IntRange(1, 100).Draw(rt, "num_events") + + events := make([]ForwardingEvent, numEvents) + timestamp := baseTime + for i := 0; i < numEvents; i++ { + amtIn := rapid.Uint64Range( + 1000, 100000).Draw(rt, "amt_in") + amtOut := rapid.Uint64Range( + 500, uint64(amtIn)).Draw(rt, "amt_out") + + events[i] = ForwardingEvent{ + Timestamp: timestamp, + IncomingChanID: lnwire.NewShortChanIDFromInt( + rapid.Uint64().Draw(rt, "in_chan"), + ), + OutgoingChanID: lnwire.NewShortChanIDFromInt( + rapid.Uint64().Draw(rt, "out_chan"), + ), + AmtIn: lnwire.MilliSatoshi(amtIn), + AmtOut: lnwire.MilliSatoshi(amtOut), + IncomingHtlcID: fn.Some(uint64(i)), + OutgoingHtlcID: fn.Some(uint64(i)), + } + // Add random interval between events (1 second to 1 + // hour). + interval := rapid.Int64Range(1, 3600).Draw( + rt, "interval", + ) + timestamp = timestamp.Add( + time.Duration(interval) * time.Second, + ) + } + + // Add events to database. + err = log.AddForwardingEvents(events) + if err != nil { + rt.Fatalf("unable to add events: %v", err) + } + + // Pick a random delete time somewhere in the middle or after. + // This gives us a mix of partial and full deletions. + deleteIndex := rapid.IntRange(0, numEvents).Draw( + rt, "delete_index", + ) + + var deleteTime time.Time + if deleteIndex < numEvents { + deleteTime = events[deleteIndex].Timestamp + } else { + deleteTime = timestamp.Add(time.Hour) + } + + // Pick a random batch size, then delete with that batch size. + batchSize := rapid.IntRange(1, 100).Draw(rt, "batch_size") + stats, err := log.DeleteForwardingEvents(deleteTime, batchSize) + if err != nil { + rt.Fatalf("delete failed: %v", err) + } + + // Invariant 1: Number of deleted events should match count + // before delete time. + expectedDeleted := 0 + var expectedFees int64 + for _, event := range events { + if event.Timestamp.Before(deleteTime) || + event.Timestamp.Equal(deleteTime) { + + expectedDeleted++ + + expectedFees += int64(event.AmtIn - event.AmtOut) + } + } + if stats.NumEventsDeleted != uint64(expectedDeleted) { + rt.Fatalf("deleted count doesn't match: expected %d, got %d", + expectedDeleted, stats.NumEventsDeleted) + } + + // Invariant 2: Total fees should equal sum of deleted event + // fees. + if stats.TotalFeeMsat != expectedFees { + rt.Fatalf("total fees don't match: expected %d, got %d", + expectedFees, stats.TotalFeeMsat) + } + + // Invariant 3: Query should only return events after delete + // time. + query := ForwardingEventQuery{ + StartTime: baseTime, + EndTime: timestamp.Add(time.Hour), + NumMaxEvents: uint32(numEvents * 2), + } + result, err := log.Query(query) + if err != nil { + rt.Fatalf("query failed: %v", err) + } + + expectedRemaining := numEvents - expectedDeleted + if len(result.ForwardingEvents) != expectedRemaining { + rt.Fatalf("wrong number of remaining events: "+ + "expected %d, got %d", + expectedRemaining, len(result.ForwardingEvents)) + } + + // Invariant 4: All remaining events should be after delete + // time. + for _, event := range result.ForwardingEvents { + if !event.Timestamp.After(deleteTime) { + rt.Fatalf("remaining event is not "+ + "after delete time: %v <= %v", + event.Timestamp, deleteTime) + } + } + + // Invariant 5: Second deletion should be idempotent. + stats2, err := log.DeleteForwardingEvents(deleteTime, batchSize) + if err != nil { + rt.Fatalf("second delete failed: %v", err) + } + if stats2.NumEventsDeleted != 0 { + rt.Fatalf("second delete should delete nothing, got %d", + stats2.NumEventsDeleted) + } + if stats2.TotalFeeMsat != 0 { + rt.Fatalf("second delete should have no fees, got %d", + stats2.TotalFeeMsat) + } + }) +} diff --git a/cmd/commands/cmd_payments.go b/cmd/commands/cmd_payments.go index d13b52da2cd..61bcd95a26f 100644 --- a/cmd/commands/cmd_payments.go +++ b/cmd/commands/cmd_payments.go @@ -1936,6 +1936,132 @@ func deletePayments(ctx *cli.Context) error { return nil } +var deleteFwdHistoryCommand = cli.Command{ + Name: "deletefwdhistory", + Category: "Payments", + Usage: "Delete old forwarding history for privacy.", + ArgsUsage: "duration | before", + Description: ` + Deletes forwarding history events older than a specified time. This is + useful for implementing data retention policies for privacy purposes. The + command permanently removes old forwarding events from the database and + returns statistics about the deletion including total fees earned. + + Time can be specified in two ways: + 1. Relative duration (standard Go or custom units): e.g., "-1w", "-24h", "-1M" + 2. Absolute Unix timestamp: e.g., "1640995200" + + Supported relative time units: + - Standard Go: ns, us/µs, ms, s, m, h (e.g., "-24h", "-1.5h") + - Custom units: d (days), w (weeks), M (months=30.44d), y (years=365.25d) + + Examples: + lncli deletefwdhistory --duration="-1M" # Delete events older than ~1 month + lncli deletefwdhistory --duration="-720h" # Delete events older than ~1 month (same) + lncli deletefwdhistory --before=1640995200 # Delete events before Jan 1, 2022 + lncli deletefwdhistory --duration="-1y" --batch_size=5000 # ~1 year + + NOTE: As with deletepayments, removing events from the database frees up + disk space within bbolt, but that space is only reclaimed after compacting + the database. Consider enabling auto-compaction (db.bolt.auto-compact=true). + + WARNING: This operation is irreversible. Deleted forwarding history cannot + be recovered. A minimum age validation is enforced to prevent accidental + deletion of very recent data. + `, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "duration", + Usage: "delete events older than this relative duration " + + `(e.g., "-1w", "-1M", "-24h", "-720h")`, + }, + cli.Uint64Flag{ + Name: "before", + Usage: "delete events before this Unix timestamp (seconds)", + }, + cli.Uint64Flag{ + Name: "batch_size", + Usage: "number of events to delete per database transaction " + + "(default: 10000, max: 50000)", + }, + }, + Action: actionDecorator(deleteFwdHistory), +} + +func deleteFwdHistory(ctx *cli.Context) error { + ctxc := getContext() + conn := getClientConn(ctx, false) + defer conn.Close() + + client := routerrpc.NewRouterClient(conn) + + // Show command help if no arguments or flags are provided. + if ctx.NArg() > 0 || (!ctx.IsSet("duration") && !ctx.IsSet("before")) { + _ = cli.ShowCommandHelp(ctx, "deletefwdhistory") + return nil + } + + // User must specify exactly one of duration or before. + if ctx.IsSet("duration") && ctx.IsSet("before") { + return fmt.Errorf("cannot use both --duration and --before; " + + "specify one time parameter") + } + + req := &routerrpc.DeleteForwardingHistoryRequest{} + + switch { + case ctx.IsSet("duration"): + req.TimeSpec = &routerrpc.DeleteForwardingHistoryRequest_Duration{ + Duration: ctx.String("duration"), + } + + case ctx.IsSet("before"): + req.TimeSpec = &routerrpc.DeleteForwardingHistoryRequest_DeleteBeforeTime{ + DeleteBeforeTime: ctx.Uint64("before"), + } + + default: + return fmt.Errorf("either --duration or --before must be specified") + } + + if ctx.IsSet("batch_size") { + req.BatchSize = uint32(ctx.Uint64("batch_size")) + } + + fmt.Println("WARNING: This operation is irreversible " + + "and will permanently delete forwarding history.") + fmt.Print("Proceed? (yes/no): ") + + var response string + _, err := fmt.Scanln(&response) + if err != nil { + return fmt.Errorf("failed to read confirmation: %w", err) + } + + if response != "yes" { + fmt.Println("Operation cancelled.") + return nil + } + + fmt.Println("Deleting forwarding history, this may take a while...") + + resp, err := client.DeleteForwardingHistory(ctxc, req) + if err != nil { + return fmt.Errorf( + "failed to delete forwarding history: %w", err, + ) + } + + fmt.Printf("\nDeletion complete:\n") + fmt.Printf(" Events deleted: %d\n", resp.EventsDeleted) + fmt.Printf(" Total fees: %d msat (%.8f BTC)\n", + resp.TotalFeeMsat, + float64(resp.TotalFeeMsat)/100000000000.0) + fmt.Printf(" Status: %s\n", resp.Status) + + return nil +} + var estimateRouteFeeCommand = cli.Command{ Name: "estimateroutefee", Category: "Payments", diff --git a/cmd/commands/main.go b/cmd/commands/main.go index a11b63b9d33..dc9b993de57 100644 --- a/cmd/commands/main.go +++ b/cmd/commands/main.go @@ -497,6 +497,7 @@ func Main() { feeReportCommand, updateChannelPolicyCommand, forwardingHistoryCommand, + deleteFwdHistoryCommand, exportChanBackupCommand, verifyChanBackupCommand, restoreChanBackupCommand, diff --git a/docs/forwarding_history_privacy.md b/docs/forwarding_history_privacy.md new file mode 100644 index 00000000000..47831b63b6e --- /dev/null +++ b/docs/forwarding_history_privacy.md @@ -0,0 +1,402 @@ +# Forwarding History Privacy Management + +## Introduction + +The Lightning Network excels at providing fast, low-cost payments with strong +privacy properties. However, routing nodes and Lightning Service Providers +(LSPs) face a unique challenge: their operational databases accumulate +forwarding history that, if compromised or subpoenaed, could reveal sensitive +information about payment flows across the network. This document explores the +privacy implications of forwarding logs and introduces LND's solution for +implementing data retention policies without migrating to a new node instance. + +## Understanding Forwarding History + +When your LND node routes a payment between two other nodes, it records detailed +information about that forwarding event in its database. This serves several +important operational purposes, including fee accounting, channel performance +analysis, and troubleshooting. Each forwarding event captures the incoming and +outgoing channels, amounts transferred, fees earned, and precise timestamps. + +Over months or years of operation, a busy routing node accumulates millions of +these records. While this historical data provides valuable insights into node +performance, it also creates a potential privacy liability. An attacker who +gains access to this database—whether through a security breach, or physical +seizure—could potentially reconstruct payment paths across the network by +correlating forwarding events across multiple compromised nodes. + +## Privacy Implications for Operators + +For individual routing node operators, the privacy risks of retaining unlimited +forwarding history are modest but real. If an adversary gains access to your +node's database, they could analyze your forwarding patterns to infer +information about the network topology you participate in and potentially +identify payment patterns involving your channels. + +### The Traditional Dilemma + +Prior to this feature, routing node operators faced an uncomfortable tradeoff. +To implement a data retention policy and purge old forwarding logs, the only +practical option was to shut down the node, reset the database, and restore +channels from backups—effectively migrating to a fresh node instance. This +process carries significant operational risks, including potential channel +closures, loss of channel state, and extended downtime. For LSPs serving +customers around the clock, such maintenance windows are highly disruptive. + +## The DeleteForwardingHistory Solution + +LND's `DeleteForwardingHistory` RPC addresses this challenge by providing a +safe, reversible-only-forward way to implement data retention policies. The +feature allows operators to specify a time threshold—either as a relative +duration or an absolute timestamp—and permanently delete all forwarding events +older than that threshold. The deletion operation executes in configurable +batches to avoid holding large database locks, and it returns statistics about +the deleted events, including the total fees earned during that period for +accounting purposes. + +### How It Works + +The deletion mechanism operates at the database layer, directly manipulating the +forwarding log bucket in LND's embedded bbolt database. The forwarding log +stores events using nanosecond-precision timestamps as keys, which enables +efficient time-based range queries. When you invoke a deletion, LND constructs a +cursor-based iteration that walks through events in chronological order, +collecting keys for events older than your specified cutoff time. It then +deletes these events in batches, with each batch executed within its own +database transaction. + +```mermaid +sequenceDiagram + participant User + participant CLI + participant Router RPC + participant ForwardingLog + participant Database + + User->>CLI: deletefwdhistory --duration="-720h" + CLI->>Router RPC: DeleteForwardingHistory(duration: "-720h") + + Router RPC->>Router RPC: Parse duration → absolute time + Router RPC->>Router RPC: Validate minimum age (1 hour) + + loop For each batch (default: 10,000 events) + Router RPC->>ForwardingLog: DeleteForwardingEvents(endTime, batchSize) + ForwardingLog->>Database: Begin transaction + ForwardingLog->>Database: Iterate events < endTime + ForwardingLog->>ForwardingLog: Calculate fees for batch + ForwardingLog->>Database: Delete batch of keys + ForwardingLog->>Database: Commit transaction + end + + ForwardingLog->>Router RPC: Return stats (deleted count, total fees) + Router RPC->>CLI: DeleteForwardingHistoryResponse + CLI->>User: Display deletion results +``` + +This batched approach ensures that even nodes with millions of forwarding events +can safely purge old data without causing database performance issues. Each +batch completes within a separate transaction, limiting lock contention and +allowing other database operations to proceed between batches. + +### Security Considerations + +The implementation includes several safeguards to prevent accidental data loss. +First, the RPC enforces a minimum age requirement: you cannot delete events less +than one hour old. This prevents mishaps where an operator accidentally deletes +recent forwarding history due to a timestamp parsing error or misunderstanding +the time format. The CLI command additionally requires explicit confirmation +before proceeding with the deletion. + +Second, the RPC requires the "offchain:write" macaroon permission, treating +forwarding history deletion as a sensitive write operation similar to payment +deletion. This ensures that only authorized users can purge forwarding data. + +Third, the operation is logged extensively. LND writes detailed log messages +before and after each deletion operation, recording the time threshold, batch +size, number of events deleted, and total fees from the deleted period. These +audit trails help operators verify that deletions executed as intended. + +### Fee Accounting + +One critical requirement for LSPs implementing data retention policies is +maintaining accurate accounting records. Even after purging old forwarding +events for privacy reasons, operators need to know how much revenue their node +generated during those periods for tax reporting and business analytics. + +The deletion operation addresses this by calculating and returning the sum of +all fees earned from the deleted events. For each event, LND computes the fee as +the difference between the incoming and outgoing amounts, then aggregates these +fees across all deleted events. The response includes this total in +millisatoshis, allowing operators to record their earnings before purging the +detailed records. + +```mermaid +graph TD + A[Forwarding Event] --> B{Calculate Fee} + B --> C[Fee = AmtIn - AmtOut] + C --> D[Accumulate to TotalFees] + D --> E{More Events?} + E -->|Yes| A + E -->|No| F[Return Total to User] + F --> G[Operator Records
for Accounting] + G --> H[Delete Detailed Events] +``` + +This approach separates accounting data from operational surveillance data. You +can maintain aggregate financial records while minimizing the detailed +forwarding logs that pose privacy risks. + +## Usage Guide + +### Command Line Interface + +The `lncli deletefwdhistory` command provides the primary interface for +operators. The command accepts time specifications in two formats: relative +durations for convenience, or absolute Unix timestamps for precision. + +For most use cases, relative durations offer the most intuitive interface. To +implement a 90-day retention policy, you would periodically run: + +```bash +lncli deletefwdhistory --duration="-90d" +``` + +The supported time units cover a wide range of retention policies: + +- Seconds (`s`) and minutes (`m`) for testing or very short-term retention +- Hours (`h`) and days (`d`) for common operational timeframes +- Weeks (`w`) for weekly cleanup schedules +- Months (`M`, averaged to 30.44 days) for typical retention policies +- Years (`y`, averaged to 365.25 days) for long-term archives + +The minus sign prefix indicates you're specifying how far back in time to +delete. This convention matches the relative time syntax used elsewhere in LND +and makes the intent clear: "delete events from more than X time ago." + +For precise control, you can specify an absolute Unix timestamp: + +```bash +lncli deletefwdhistory --before=1704067200 +``` + +This deletes all events before January 1, 2024 00:00:00 UTC. Absolute timestamps +are particularly useful when implementing policies tied to specific dates, such +as calendar year boundaries for accounting purposes or regulatory compliance +deadlines. + +### Batch Size Tuning + +The `--batch_size` flag controls how many events are deleted per database +transaction. The default value of 10,000 provides a good balance for most nodes, +but you may want to adjust this based on your node's characteristics. + +For nodes with slower disk I/O or running on resource-constrained hardware, +reducing the batch size decreases the duration of each database lock, improving +responsiveness to concurrent operations: + +```bash +lncli deletefwdhistory --duration="-1M" --batch_size=5000 +``` + +Conversely, for nodes with fast SSDs and low concurrent load, increasing the +batch size can speed up the overall deletion process: + +```bash +lncli deletefwdhistory --duration="-1M" --batch_size=25000 +``` + +The implementation caps the maximum batch size at 50,000 to prevent excessively +large transactions from degrading database performance. + +### Automation and Scheduling + +Most operators will want to automate forwarding history cleanup rather than +running deletions manually. The command integrates naturally with cron jobs or +systemd timers. For a monthly cleanup maintaining a 90-day retention window: + +```bash +# Run at 3 AM on the first day of each month +0 3 1 * * /usr/local/bin/lncli deletefwdhistory --duration="-90d" >> /var/log/lnd/fwdhistory_cleanup.log 2>&1 +``` + +Note that the automated script omits the interactive confirmation prompt. In +production automation, you should implement additional safeguards such as +pre-deletion validation checks and alerting on unexpected results. + +For more sophisticated automation, consider implementing a script that: + +1. Queries current forwarding history statistics +2. Calculates the appropriate deletion threshold based on database size and growth rate +3. Executes the deletion +4. Records the fees returned for accounting +5. Monitors the resulting database size and alerts if disk space isn't reclaimed as expected + +### Database Compaction + +Deleting forwarding events frees space within LND's bbolt database, but this +space isn't immediately returned to the operating system. bbolt uses a +copy-on-write structure where deleted data leaves "free pages" that can be +reused for future writes, but the overall file size doesn't shrink until you +compact the database. + +LND supports automatic compaction via the configuration option: + +``` +db.bolt.auto-compact=true +``` + +With auto-compaction enabled, LND periodically performs compaction during normal +operation, typically triggered when the amount of free space exceeds a +threshold. However, after a large deletion operation, you may want to trigger +compaction immediately to reclaim disk space. + +The recommended approach is to schedule compaction shortly after your regular +deletion operations: + +1. Run `deletefwdhistory` to purge old events +2. Restart LND with `--db.bolt.auto-compact=true` if not already enabled +3. Monitor database file size to confirm space reclamation + +Be aware that database compaction requires free disk space equal to the current +database size during the operation, as it creates a new, compacted copy of the +database before replacing the original. + +## Integration with Existing Tools + +### Forwarding History Analysis + +The deletion operation doesn't interfere with LND's existing `forwardinghistory` +RPC, which allows you to query and analyze forwarding events. After a deletion, +queries for time ranges that have been purged will simply return no events for +those periods, while more recent events remain accessible. + +This means you can continue using analytical tools and scripts that query +forwarding history, but you should design them to handle sparse historical data +gracefully. Tools should not assume that forwarding history extends back to the +node's inception date. + +### Channel Analytics + +Similarly, channel performance analysis tools that rely on forwarding history +will only have access to events within your retention window. When evaluating +channel performance metrics like forwarding frequency or fee revenue, be mindful +that historical data before your retention cutoff is no longer available. + +For long-term performance tracking, consider aggregating statistics before +purging detailed events. You might maintain summary records showing weekly or +monthly aggregate forwarding counts and fees per channel, even after deleting +the individual event records. + +## Privacy Best Practices + +While the deletion feature provides operators with a mechanism to implement data +retention policies, it's important to understand what it does and doesn't +protect against. + +### What Deletion Protects + +Deleting old forwarding history reduces your node's exposure if the database is +compromised in the future. An attacker who gains access to your node after +you've implemented a 90-day retention policy can only observe the last 90 days +of forwarding activity, not the entire operational history. This limits the +window during which surveillance or correlation attacks could be performed using +your node's data. + +### What Deletion Doesn't Protect + +The revocation log for _active_ channels contains information that can be used +to reconstruct transaction flows. Once channels are closed, this data is +automatically deleted. + +The normal logs of a node also contain information that can be used to correlate +transactions. Users can set up automated systems to manually purge logs, or +configure the logging directory to a purely in-memory file system. + +### Defense in Depth + +Forwarding history deletion should be one component of a comprehensive privacy +strategy, not your only defense. Other important measures include: + +- Restricting physical and network access to the node +- Implementing strong authentication and access controls +- Regularly auditing who has access to the node and its backups +- Using channel aliases and avoiding personally identifiable information in + channel names +- Running your node over Tor to hide the network-level correlation between node + identity and IP address + + +The deletion feature gives you control over how long your node retains detailed +forwarding records, but it doesn't eliminate all privacy risks inherent in +operating a Lightning Network routing node. + +## Troubleshooting + +### Database Lock Timeouts + +During deletion of very large numbers of events, you might encounter database +lock timeout errors if other operations are trying to access the database +concurrently. If this occurs: + +1. Reduce the batch size to shorten each transaction +2. Schedule deletions during low-traffic periods +3. Temporarily pause other operations that query forwarding history frequently + +### Insufficient Disk Space for Compaction + +Database compaction requires temporary free space roughly equal to the size of +your database. If compaction fails due to insufficient disk space, you'll need +to free up space before the compaction can proceed: + +1. Delete other unnecessary files from the disk +2. Move log files or other non-critical data to alternate storage +3. Consider whether you can safely delete older database backups + +## Performance Considerations + +Deletion performance scales linearly with the number of events being deleted. On +a node with fast SSD storage, expect deletion rates of approximately 100,000 +events per second with default batch sizes. A typical cleanup deleting one +million events should complete in under a minute. + +The operation's impact on node performance during deletion is minimal. Each +batch executes quickly, and the gaps between batches allow other database +operations to proceed. You can safely run deletions while the node is actively +routing payments, though you may want to avoid doing so during peak traffic +times on very busy nodes. + +Database compaction has a more significant performance impact, as it requires +LND to copy the entire database. During compaction, expect elevated CPU and disk +I/O, and budget several minutes for the operation to complete depending on your +database size. LND remains operational during compaction, but you may observe +increased latency for database-heavy operations. + +## Future Enhancements + +The current implementation provides the core functionality needed for +privacy-conscious forwarding history management, but several enhancements could +further improve the feature: + +**Selective Deletion**: Future versions might support deleting events based on +criteria beyond just time, such as deleting forwards below a certain value +threshold or forwards involving specific channels. This would allow more nuanced +retention policies that preserve high-value or otherwise interesting events +while purging routine traffic. + +**Automatic Scheduled Deletion**: Rather than requiring operators to set up +external cron jobs, LND could include built-in support for configuring retention +policies that execute automatically. The configuration might specify a retention +period and check interval, with the daemon handling cleanup internally. + +**Privacy-Preserving Aggregates**: Instead of deleting forwarding history +entirely, the system could support automatically aggregating old events into +coarser summaries that preserve analytical value while reducing privacy +exposure. For example, events older than 90 days could be combined into +per-channel weekly statistics, retaining information about channel performance +without preserving individual forwarding events. + +**Encrypted Archives**: For operators who need to preserve old forwarding data +for compliance but want to minimize its exposure, LND could support encrypting +and archiving old events before deleting them from the active database. The +encrypted archives could be decrypted only when required for audits, providing a +balance between compliance and privacy. diff --git a/itest/list_on_test.go b/itest/list_on_test.go index 92c6547b3af..7e095056e64 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -443,6 +443,10 @@ var allTestCases = []*lntest.TestCase{ Name: "forward interceptor restart", TestFunc: testForwardInterceptorRestart, }, + { + Name: "delete forwarding history", + TestFunc: testDeleteForwardingHistory, + }, { Name: "invoice HTLC modifier basic", TestFunc: testInvoiceHtlcModifierBasic, diff --git a/itest/lnd_forward_delete_test.go b/itest/lnd_forward_delete_test.go new file mode 100644 index 00000000000..c1596ef9da5 --- /dev/null +++ b/itest/lnd_forward_delete_test.go @@ -0,0 +1,430 @@ +package itest + +import ( + "fmt" + "testing" + "time" + + "github.com/btcsuite/btcd/btcutil" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/routerrpc" + "github.com/lightningnetwork/lnd/lntest" + "github.com/stretchr/testify/require" +) + +// testDeleteForwardingHistory tests the deletion of forwarding history events. +func testDeleteForwardingHistory(ht *lntest.HarnessTest) { + // Run subtests for different deletion scenarios. + testCases := []struct { + name string + test func(ht *lntest.HarnessTest) + }{ + { + name: "basic deletion", + test: testBasicDeletion, + }, + { + name: "partial deletion", + test: testPartialDeletion, + }, + { + name: "empty database", + test: testEmptyDatabaseDeletion, + }, + { + name: "idempotency", + test: testDeletionIdempotency, + }, + { + name: "time formats", + test: testTimeFormats, + }, + } + + for _, tc := range testCases { + tc := tc + success := ht.Run(tc.name, func(t *testing.T) { + st := ht.Subtest(t) + tc.test(st) + }) + + if !success { + return + } + } +} + +// testBasicDeletion tests basic forwarding history deletion functionality. +func testBasicDeletion(ht *lntest.HarnessTest) { + // Create a three-hop network: Alice -> Bob -> Carol. + const chanAmt = btcutil.Amount(300000) + p := lntest.OpenChannelParams{Amt: chanAmt} + + cfgs := [][]string{nil, nil, nil} + chanPoints, nodes := ht.CreateSimpleNetwork(cfgs, p) + alice, bob, carol := nodes[0], nodes[1], nodes[2] + _ = chanPoints + + const numPayments = 10 + const paymentAmt = 1000 + + // Send multiple payments from Alice to Carol through Bob. Sleep after + // each payment to ensure minimum age validation. + for i := 0; i < numPayments; i++ { + invoice := carol.RPC.AddInvoice(&lnrpc.Invoice{ + ValueMsat: paymentAmt, + Memo: fmt.Sprintf("test payment %d", i), + }) + + payReq := &routerrpc.SendPaymentRequest{ + PaymentRequest: invoice.PaymentRequest, + TimeoutSeconds: 60, + FeeLimitMsat: 100000, + } + + ht.SendPaymentAssertSettled(alice, payReq) + + // Sleep to ensure events are old enough. + time.Sleep(time.Second) + } + + // Sleep an additional 2 seconds to ensure all events are old enough. + time.Sleep(2 * time.Second) + + // Query Bob's forwarding history to verify events exist. + fwdHistory := bob.RPC.ForwardingHistory(nil) + require.Len( + ht, fwdHistory.ForwardingEvents, numPayments, + "expected %d forwarding events", numPayments, + ) + + // Calculate expected total fees. + var expectedFees int64 + for _, event := range fwdHistory.ForwardingEvents { + expectedFees += int64(event.FeeMsat) + } + + // Record the timestamp of the last event for testing. + // + //nolint:lll + lastTimestamp := fwdHistory.ForwardingEvents[len(fwdHistory.ForwardingEvents)-1].TimestampNs + + // Delete all forwarding events using a timestamp that's 2 seconds in + // the past to satisfy the minimum age validation. + delResp := bob.RPC.DeleteForwardingHistory( + &routerrpc.DeleteForwardingHistoryRequest{ + TimeSpec: &routerrpc.DeleteForwardingHistoryRequest_DeleteBeforeTime{ + DeleteBeforeTime: uint64(time.Now().Add( + -2 * time.Second).Unix(), + ), + }, + }, + ) + + // Verify deletion statistics. + require.Equal( + ht, uint64(numPayments), delResp.EventsDeleted, + "wrong number of events deleted", + ) + require.Equal( + ht, expectedFees, delResp.TotalFeeMsat, + "wrong total fees", + ) + require.Contains( + ht, delResp.Status, "Successfully deleted", + "unexpected status message", + ) + + // Query forwarding history again to verify events are deleted. + fwdHistoryAfter := bob.RPC.ForwardingHistory(nil) + require.Empty( + ht, fwdHistoryAfter.ForwardingEvents, + "forwarding events should be deleted", + ) + + // Verify that the last event timestamp is no longer in the history. + fwdHistorySpecific := bob.RPC.ForwardingHistory( + &lnrpc.ForwardingHistoryRequest{ + StartTime: 0, + EndTime: lastTimestamp, + }, + ) + require.Empty( + ht, fwdHistorySpecific.ForwardingEvents, + "specific time range query should return no events", + ) +} + +// testPartialDeletion tests deleting only a subset of forwarding events. +func testPartialDeletion(ht *lntest.HarnessTest) { + // Create a three-hop network: Alice -> Bob -> Carol. + const chanAmt = btcutil.Amount(300000) + p := lntest.OpenChannelParams{Amt: chanAmt} + + cfgs := [][]string{nil, nil, nil} + chanPoints, nodes := ht.CreateSimpleNetwork(cfgs, p) + alice, bob, carol := nodes[0], nodes[1], nodes[2] + _ = chanPoints + + const firstBatch = 5 + const paymentAmt = 1000 + + // Send first batch of payments. + for i := 0; i < firstBatch; i++ { + invoice := carol.RPC.AddInvoice(&lnrpc.Invoice{ + ValueMsat: paymentAmt, + Memo: fmt.Sprintf("batch 1 payment %d", i), + }) + + payReq := &routerrpc.SendPaymentRequest{ + PaymentRequest: invoice.PaymentRequest, + TimeoutSeconds: 60, + FeeLimitMsat: 100000, + } + + ht.SendPaymentAssertSettled(alice, payReq) + + // Sleep to ensure events are old enough. + time.Sleep(time.Second) + } + + // Record the timestamp after first batch. + cutoffTime := time.Now() + + // Send a second batch of payments. + const secondBatch = 5 + for i := 0; i < secondBatch; i++ { + invoice := carol.RPC.AddInvoice(&lnrpc.Invoice{ + ValueMsat: paymentAmt, + Memo: fmt.Sprintf("batch 2 payment %d", i), + }) + + payReq := &routerrpc.SendPaymentRequest{ + PaymentRequest: invoice.PaymentRequest, + TimeoutSeconds: 60, + FeeLimitMsat: 100000, + } + + ht.SendPaymentAssertSettled(alice, payReq) + } + + // Query Bob's forwarding history to verify all events exist. + fwdHistory := bob.RPC.ForwardingHistory(nil) + require.Len( + ht, fwdHistory.ForwardingEvents, firstBatch+secondBatch, + "expected total events before deletion", + ) + + // Delete only the first batch of events using the cutoff time. + delResp := bob.RPC.DeleteForwardingHistory( + &routerrpc.DeleteForwardingHistoryRequest{ + TimeSpec: &routerrpc.DeleteForwardingHistoryRequest_DeleteBeforeTime{ + DeleteBeforeTime: uint64(cutoffTime.Unix()), + }, + }, + ) + + // Should have deleted approximately the first batch. + require.LessOrEqual( + ht, delResp.EventsDeleted, uint64(firstBatch), + "deleted more events than expected", + ) + require.Greater( + ht, delResp.EventsDeleted, uint64(0), + "should have deleted some events", + ) + + // Query forwarding history to verify second batch remains. + fwdHistoryAfter := bob.RPC.ForwardingHistory(nil) + require.NotEmpty( + ht, fwdHistoryAfter.ForwardingEvents, + "some forwarding events should remain", + ) + require.GreaterOrEqual( + ht, len(fwdHistoryAfter.ForwardingEvents), secondBatch-1, + "at least most of second batch should remain", + ) +} + +// testEmptyDatabaseDeletion tests deletion on an empty forwarding log. +func testEmptyDatabaseDeletion(ht *lntest.HarnessTest) { + // Create a standalone node (no channels, no forwards). + bob := ht.NewNode("Bob", nil) + + // Try to delete from empty database using custom duration format. + delResp := bob.RPC.DeleteForwardingHistory( + &routerrpc.DeleteForwardingHistoryRequest{ + TimeSpec: &routerrpc.DeleteForwardingHistoryRequest_Duration{ + Duration: "-1d", + }, + }, + ) + + // Should successfully handle empty database. + require.Equal( + ht, uint64(0), delResp.EventsDeleted, + "should delete 0 events from empty database", + ) + require.Equal( + ht, int64(0), delResp.TotalFeeMsat, + "should have 0 fees from empty database", + ) +} + +// testDeletionIdempotency tests that deletion is idempotent. +func testDeletionIdempotency(ht *lntest.HarnessTest) { + // Create a three-hop network: Alice -> Bob -> Carol. + const chanAmt = btcutil.Amount(300000) + p := lntest.OpenChannelParams{Amt: chanAmt} + + cfgs := [][]string{nil, nil, nil} + chanPoints, nodes := ht.CreateSimpleNetwork(cfgs, p) + alice, bob, carol := nodes[0], nodes[1], nodes[2] + _ = chanPoints + + // Send a few payments to create forwarding events. + const numPayments = 5 + const paymentAmt = 1000 + + for i := 0; i < numPayments; i++ { + invoice := carol.RPC.AddInvoice(&lnrpc.Invoice{ + ValueMsat: paymentAmt, + Memo: fmt.Sprintf("payment %d", i), + }) + + payReq := &routerrpc.SendPaymentRequest{ + PaymentRequest: invoice.PaymentRequest, + TimeoutSeconds: 60, + FeeLimitMsat: 100000, + } + + ht.SendPaymentAssertSettled(alice, payReq) + + // Sleep to ensure events are old enough. + time.Sleep(time.Second) + } + + // Sleep an additional 2 seconds to ensure all events are old enough. + time.Sleep(2 * time.Second) + + // Verify events exist. + fwdHistory := bob.RPC.ForwardingHistory(nil) + require.Len( + ht, fwdHistory.ForwardingEvents, numPayments, + "expected forwarding events before deletion", + ) + + // Delete all events using a timestamp 2 seconds in the past. + deleteTime := uint64(time.Now().Add(-2 * time.Second).Unix()) + + delResp1 := bob.RPC.DeleteForwardingHistory( + &routerrpc.DeleteForwardingHistoryRequest{ + TimeSpec: &routerrpc.DeleteForwardingHistoryRequest_DeleteBeforeTime{ + DeleteBeforeTime: deleteTime, + }, + }, + ) + + require.Equal( + ht, uint64(numPayments), delResp1.EventsDeleted, + "first deletion should delete all events", + ) + + // Delete again with same parameters. + delResp2 := bob.RPC.DeleteForwardingHistory( + &routerrpc.DeleteForwardingHistoryRequest{ + TimeSpec: &routerrpc.DeleteForwardingHistoryRequest_DeleteBeforeTime{ + DeleteBeforeTime: deleteTime, + }, + }, + ) + + // Second deletion should delete nothing (idempotent). + require.Equal( + ht, uint64(0), delResp2.EventsDeleted, + "second deletion should delete 0 events (idempotent)", + ) + require.Equal( + ht, int64(0), delResp2.TotalFeeMsat, + "second deletion should have 0 fees", + ) +} + +// testTimeFormats tests different time specification formats. +func testTimeFormats(ht *lntest.HarnessTest) { + // Create a three-hop network: Alice -> Bob -> Carol. + const chanAmt = btcutil.Amount(300000) + p := lntest.OpenChannelParams{Amt: chanAmt} + + cfgs := [][]string{nil, nil, nil} + chanPoints, nodes := ht.CreateSimpleNetwork(cfgs, p) + alice, bob, carol := nodes[0], nodes[1], nodes[2] + _ = chanPoints + + // Helper function to create forwarding events. + createForwards := func(count int) { + for i := 0; i < count; i++ { + invoice := carol.RPC.AddInvoice(&lnrpc.Invoice{ + ValueMsat: 1000, + Memo: fmt.Sprintf("payment %d", i), + }) + + payReq := &routerrpc.SendPaymentRequest{ + PaymentRequest: invoice.PaymentRequest, + TimeoutSeconds: 60, + FeeLimitMsat: 100000, + } + + ht.SendPaymentAssertSettled(alice, payReq) + + // Sleep to ensure events are old enough. + time.Sleep(time.Second) + } + + // Sleep an additional 2 seconds to ensure all events are old + // enough. + time.Sleep(2 * time.Second) + } + + // Test relative duration format. + createForwards(3) + + // Use duration format. Events are just created, so "-1d" (1 day ago) + // will not delete them. + delResp := bob.RPC.DeleteForwardingHistory( + &routerrpc.DeleteForwardingHistoryRequest{ + TimeSpec: &routerrpc.DeleteForwardingHistoryRequest_Duration{ + Duration: "-1d", + }, + }, + ) + + // Should delete nothing since events are recent. + require.Equal( + ht, uint64(0), delResp.EventsDeleted, + "recent events should not be deleted with -1d duration", + ) + + // Test absolute timestamp format. Query current events and use a + // timestamp 2 seconds in the past. + fwdHistory2 := bob.RPC.ForwardingHistory(nil) + require.Len( + ht, fwdHistory2.ForwardingEvents, 3, + "expected 3 events before second deletion", + ) + + delResp2 := bob.RPC.DeleteForwardingHistory( + &routerrpc.DeleteForwardingHistoryRequest{ + TimeSpec: &routerrpc.DeleteForwardingHistoryRequest_DeleteBeforeTime{ + DeleteBeforeTime: uint64( + time.Now().Add(-2 * time.Second).Unix(), + ), + }, + }, + ) + + require.Equal( + ht, uint64(3), delResp2.EventsDeleted, + "absolute timestamp should delete all events", + ) +} diff --git a/lnrpc/routerrpc/parse_duration_test.go b/lnrpc/routerrpc/parse_duration_test.go new file mode 100644 index 00000000000..637b0183cb7 --- /dev/null +++ b/lnrpc/routerrpc/parse_duration_test.go @@ -0,0 +1,243 @@ +package routerrpc + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/require" + "pgregory.net/rapid" +) + +// TestParseDuration tests the hybrid duration parsing with explicit examples. +func TestParseDuration(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + expected time.Duration + wantErr bool + }{ + // Standard Go durations. + { + name: "standard go hours", + input: "-24h", + expected: -24 * time.Hour, + }, + { + name: "standard go fractional hours", + input: "-1.5h", + expected: time.Duration(-1.5 * float64(time.Hour)), + }, + { + name: "standard go minutes", + input: "-30m", + expected: -30 * time.Minute, + }, + { + name: "standard go seconds", + input: "-60s", + expected: -60 * time.Second, + }, + { + name: "standard go milliseconds", + input: "-500ms", + expected: -500 * time.Millisecond, + }, + { + name: "standard go microseconds", + input: "-1000us", + expected: -1000 * time.Microsecond, + }, + { + name: "standard go complex", + input: "-2h30m45s", + expected: -(2*time.Hour + 30*time.Minute + 45*time.Second), + }, + + // Custom units. + { + name: "custom days", + input: "-1d", + expected: -24 * time.Hour, + }, + { + name: "custom multiple days", + input: "-7d", + expected: -7 * 24 * time.Hour, + }, + { + name: "custom weeks", + input: "-1w", + expected: -7 * 24 * time.Hour, + }, + { + name: "custom multiple weeks", + input: "-4w", + expected: -4 * 7 * 24 * time.Hour, + }, + { + name: "custom months", + input: "-1M", + expected: time.Duration(-30.44 * 24 * float64(time.Hour)), + }, + { + name: "custom years", + input: "-1y", + expected: time.Duration(-365.25 * 24 * float64(time.Hour)), + }, + + // Error cases. + { + name: "positive duration", + input: "1h", + wantErr: true, + }, + { + name: "no minus sign custom", + input: "1d", + wantErr: true, + }, + { + name: "empty string", + input: "", + wantErr: true, + }, + { + name: "just minus", + input: "-", + wantErr: true, + }, + { + name: "no number", + input: "-d", + wantErr: true, + }, + { + name: "no unit", + input: "-5", + wantErr: true, + }, + { + name: "invalid unit", + input: "-1x", + wantErr: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got, err := parseDuration(tt.input) + if tt.wantErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + require.Equal(t, tt.expected, got) + }) + } +} + +// TestParseDurationProperties uses property-based testing to verify invariants. +func TestParseDurationProperties(t *testing.T) { + t.Parallel() + + // Test that all valid standard Go durations work. + t.Run("standard go durations", func(t *testing.T) { + rapid.Check(t, func(rt *rapid.T) { + // Generate a random duration string using + // time.Duration's String() method. + hours := rapid.IntRange(-8760, -1).Draw(rt, "hours") + minutes := rapid.IntRange(0, 59).Draw(rt, "minutes") + seconds := rapid.IntRange(0, 59).Draw(rt, "seconds") + + d := time.Duration(hours)*time.Hour + + time.Duration(minutes)*time.Minute + + time.Duration(seconds)*time.Second + + // Parse it back. + parsed, err := parseDuration(d.String()) + require.NoError(rt, err) + + // Should match original (within a small margin for + // float precision). + diff := parsed - d + if diff < 0 { + diff = -diff + } + require.Less( + rt, diff, time.Microsecond, + "parsed duration differs: got %v, want %v", + parsed, d, + ) + }) + }) + + // Test that custom units produce negative durations. + t.Run("custom units negative", func(t *testing.T) { + rapid.Check(t, func(rt *rapid.T) { + value := rapid.IntRange(1, 1000).Draw(rt, "value") + unit := rapid.SampledFrom([]string{"d", "w", "M", "y"}). + Draw(rt, "unit") + + input := fmt.Sprintf("-%d%s", value, unit) + + parsed, err := parseDuration(input) + require.NoError(rt, err) + require.Less( + rt, parsed, time.Duration(0), + "custom unit should produce negative duration", + ) + }) + }) + + // Test that days are always 24 hours. + t.Run("days invariant", func(t *testing.T) { + rapid.Check(t, func(rt *rapid.T) { + days := rapid.IntRange(1, 365).Draw(rt, "days") + input := fmt.Sprintf("-%dd", days) + + parsed, err := parseDuration(input) + require.NoError(rt, err) + + expected := time.Duration(-days) * 24 * time.Hour + require.Equal(rt, expected, parsed) + }) + }) + + // Test that weeks are always 7 days. + t.Run("weeks invariant", func(t *testing.T) { + rapid.Check(t, func(rt *rapid.T) { + weeks := rapid.IntRange(1, 52).Draw(rt, "weeks") + input := fmt.Sprintf("-%dw", weeks) + + parsed, err := parseDuration(input) + require.NoError(rt, err) + + expected := time.Duration(-weeks) * 7 * 24 * time.Hour + require.Equal(rt, expected, parsed) + }) + }) + + // Test that positive durations always error. + t.Run("positive durations error", func(t *testing.T) { + rapid.Check(t, func(rt *rapid.T) { + value := rapid.IntRange(1, 1000).Draw(rt, "value") + unit := rapid.SampledFrom([]string{ + "s", "m", "h", "d", "w", "M", "y", + }).Draw(rt, "unit") + + // Positive duration (no minus sign). + input := fmt.Sprintf("%d%s", value, unit) + + _, err := parseDuration(input) + require.Error(rt, err, + "positive duration should error: %s", input) + }) + }) +} diff --git a/lnrpc/routerrpc/router.pb.go b/lnrpc/routerrpc/router.pb.go index a4497c2bb5d..18eb9eaf124 100644 --- a/lnrpc/routerrpc/router.pb.go +++ b/lnrpc/routerrpc/router.pb.go @@ -3726,6 +3726,174 @@ func (x *FindBaseAliasResponse) GetBase() uint64 { return 0 } +type DeleteForwardingHistoryRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Specify the time before which to delete forwarding events using one of + // the following options: + // + // Types that are assignable to TimeSpec: + // + // *DeleteForwardingHistoryRequest_DeleteBeforeTime + // *DeleteForwardingHistoryRequest_Duration + TimeSpec isDeleteForwardingHistoryRequest_TimeSpec `protobuf_oneof:"time_spec"` + // Batch size for deletion (default 10000, max 50000). + // Controls how many events are deleted per database transaction to avoid + // holding large locks. + BatchSize uint32 `protobuf:"varint,3,opt,name=batch_size,json=batchSize,proto3" json:"batch_size,omitempty"` +} + +func (x *DeleteForwardingHistoryRequest) Reset() { + *x = DeleteForwardingHistoryRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_routerrpc_router_proto_msgTypes[47] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DeleteForwardingHistoryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteForwardingHistoryRequest) ProtoMessage() {} + +func (x *DeleteForwardingHistoryRequest) ProtoReflect() protoreflect.Message { + mi := &file_routerrpc_router_proto_msgTypes[47] + 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 DeleteForwardingHistoryRequest.ProtoReflect.Descriptor instead. +func (*DeleteForwardingHistoryRequest) Descriptor() ([]byte, []int) { + return file_routerrpc_router_proto_rawDescGZIP(), []int{47} +} + +func (m *DeleteForwardingHistoryRequest) GetTimeSpec() isDeleteForwardingHistoryRequest_TimeSpec { + if m != nil { + return m.TimeSpec + } + return nil +} + +func (x *DeleteForwardingHistoryRequest) GetDeleteBeforeTime() uint64 { + if x, ok := x.GetTimeSpec().(*DeleteForwardingHistoryRequest_DeleteBeforeTime); ok { + return x.DeleteBeforeTime + } + return 0 +} + +func (x *DeleteForwardingHistoryRequest) GetDuration() string { + if x, ok := x.GetTimeSpec().(*DeleteForwardingHistoryRequest_Duration); ok { + return x.Duration + } + return "" +} + +func (x *DeleteForwardingHistoryRequest) GetBatchSize() uint32 { + if x != nil { + return x.BatchSize + } + return 0 +} + +type isDeleteForwardingHistoryRequest_TimeSpec interface { + isDeleteForwardingHistoryRequest_TimeSpec() +} + +type DeleteForwardingHistoryRequest_DeleteBeforeTime struct { + // Absolute Unix timestamp (seconds) - delete events before this time. + DeleteBeforeTime uint64 `protobuf:"varint,1,opt,name=delete_before_time,json=deleteBeforeTime,proto3,oneof"` +} + +type DeleteForwardingHistoryRequest_Duration struct { + // Relative duration string (e.g., "-1w", "-1m", "-1y"). + // Supports: s(seconds), m(minutes), h(hours), d(days), w(weeks), + // M(months), y(years). Month equals 30.44 days, year equals 365.25 + // days. + Duration string `protobuf:"bytes,2,opt,name=duration,proto3,oneof"` +} + +func (*DeleteForwardingHistoryRequest_DeleteBeforeTime) isDeleteForwardingHistoryRequest_TimeSpec() {} + +func (*DeleteForwardingHistoryRequest_Duration) isDeleteForwardingHistoryRequest_TimeSpec() {} + +type DeleteForwardingHistoryResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Number of forwarding events deleted. + EventsDeleted uint64 `protobuf:"varint,1,opt,name=events_deleted,json=eventsDeleted,proto3" json:"events_deleted,omitempty"` + // Total fees earned from deleted events (in millisatoshis). + // This is the sum of (amt_in - amt_out) for all deleted events, which + // can be used for accounting purposes. + TotalFeeMsat int64 `protobuf:"varint,2,opt,name=total_fee_msat,json=totalFeeMsat,proto3" json:"total_fee_msat,omitempty"` + // Status message. + Status string `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"` +} + +func (x *DeleteForwardingHistoryResponse) Reset() { + *x = DeleteForwardingHistoryResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_routerrpc_router_proto_msgTypes[48] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DeleteForwardingHistoryResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteForwardingHistoryResponse) ProtoMessage() {} + +func (x *DeleteForwardingHistoryResponse) ProtoReflect() protoreflect.Message { + mi := &file_routerrpc_router_proto_msgTypes[48] + 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 DeleteForwardingHistoryResponse.ProtoReflect.Descriptor instead. +func (*DeleteForwardingHistoryResponse) Descriptor() ([]byte, []int) { + return file_routerrpc_router_proto_rawDescGZIP(), []int{48} +} + +func (x *DeleteForwardingHistoryResponse) GetEventsDeleted() uint64 { + if x != nil { + return x.EventsDeleted + } + return 0 +} + +func (x *DeleteForwardingHistoryResponse) GetTotalFeeMsat() int64 { + if x != nil { + return x.TotalFeeMsat + } + return 0 +} + +func (x *DeleteForwardingHistoryResponse) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + var File_routerrpc_router_proto protoreflect.FileDescriptor var file_routerrpc_router_proto_rawDesc = []byte{ @@ -4238,180 +4406,205 @@ var file_routerrpc_router_proto_rawDesc = []byte{ 0x6c, 0x69, 0x61, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x22, 0x2b, 0x0a, 0x15, 0x46, 0x69, 0x6e, 0x64, 0x42, 0x61, 0x73, 0x65, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x61, - 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x62, 0x61, 0x73, 0x65, 0x2a, 0x81, - 0x04, 0x0a, 0x0d, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, - 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0d, 0x0a, - 0x09, 0x4e, 0x4f, 0x5f, 0x44, 0x45, 0x54, 0x41, 0x49, 0x4c, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, - 0x4f, 0x4e, 0x49, 0x4f, 0x4e, 0x5f, 0x44, 0x45, 0x43, 0x4f, 0x44, 0x45, 0x10, 0x02, 0x12, 0x15, - 0x0a, 0x11, 0x4c, 0x49, 0x4e, 0x4b, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x45, 0x4c, 0x49, 0x47, 0x49, - 0x42, 0x4c, 0x45, 0x10, 0x03, 0x12, 0x14, 0x0a, 0x10, 0x4f, 0x4e, 0x5f, 0x43, 0x48, 0x41, 0x49, - 0x4e, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x10, 0x04, 0x12, 0x14, 0x0a, 0x10, 0x48, - 0x54, 0x4c, 0x43, 0x5f, 0x45, 0x58, 0x43, 0x45, 0x45, 0x44, 0x53, 0x5f, 0x4d, 0x41, 0x58, 0x10, - 0x05, 0x12, 0x18, 0x0a, 0x14, 0x49, 0x4e, 0x53, 0x55, 0x46, 0x46, 0x49, 0x43, 0x49, 0x45, 0x4e, - 0x54, 0x5f, 0x42, 0x41, 0x4c, 0x41, 0x4e, 0x43, 0x45, 0x10, 0x06, 0x12, 0x16, 0x0a, 0x12, 0x49, - 0x4e, 0x43, 0x4f, 0x4d, 0x50, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x46, 0x4f, 0x52, 0x57, 0x41, 0x52, - 0x44, 0x10, 0x07, 0x12, 0x13, 0x0a, 0x0f, 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x41, 0x44, 0x44, 0x5f, - 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x08, 0x12, 0x15, 0x0a, 0x11, 0x46, 0x4f, 0x52, 0x57, - 0x41, 0x52, 0x44, 0x53, 0x5f, 0x44, 0x49, 0x53, 0x41, 0x42, 0x4c, 0x45, 0x44, 0x10, 0x09, 0x12, - 0x14, 0x0a, 0x10, 0x49, 0x4e, 0x56, 0x4f, 0x49, 0x43, 0x45, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, - 0x4c, 0x45, 0x44, 0x10, 0x0a, 0x12, 0x15, 0x0a, 0x11, 0x49, 0x4e, 0x56, 0x4f, 0x49, 0x43, 0x45, - 0x5f, 0x55, 0x4e, 0x44, 0x45, 0x52, 0x50, 0x41, 0x49, 0x44, 0x10, 0x0b, 0x12, 0x1b, 0x0a, 0x17, - 0x49, 0x4e, 0x56, 0x4f, 0x49, 0x43, 0x45, 0x5f, 0x45, 0x58, 0x50, 0x49, 0x52, 0x59, 0x5f, 0x54, - 0x4f, 0x4f, 0x5f, 0x53, 0x4f, 0x4f, 0x4e, 0x10, 0x0c, 0x12, 0x14, 0x0a, 0x10, 0x49, 0x4e, 0x56, - 0x4f, 0x49, 0x43, 0x45, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x4f, 0x50, 0x45, 0x4e, 0x10, 0x0d, 0x12, - 0x17, 0x0a, 0x13, 0x4d, 0x50, 0x50, 0x5f, 0x49, 0x4e, 0x56, 0x4f, 0x49, 0x43, 0x45, 0x5f, 0x54, - 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x10, 0x0e, 0x12, 0x14, 0x0a, 0x10, 0x41, 0x44, 0x44, 0x52, - 0x45, 0x53, 0x53, 0x5f, 0x4d, 0x49, 0x53, 0x4d, 0x41, 0x54, 0x43, 0x48, 0x10, 0x0f, 0x12, 0x16, - 0x0a, 0x12, 0x53, 0x45, 0x54, 0x5f, 0x54, 0x4f, 0x54, 0x41, 0x4c, 0x5f, 0x4d, 0x49, 0x53, 0x4d, - 0x41, 0x54, 0x43, 0x48, 0x10, 0x10, 0x12, 0x15, 0x0a, 0x11, 0x53, 0x45, 0x54, 0x5f, 0x54, 0x4f, - 0x54, 0x41, 0x4c, 0x5f, 0x54, 0x4f, 0x4f, 0x5f, 0x4c, 0x4f, 0x57, 0x10, 0x11, 0x12, 0x10, 0x0a, - 0x0c, 0x53, 0x45, 0x54, 0x5f, 0x4f, 0x56, 0x45, 0x52, 0x50, 0x41, 0x49, 0x44, 0x10, 0x12, 0x12, - 0x13, 0x0a, 0x0f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x5f, 0x49, 0x4e, 0x56, 0x4f, 0x49, - 0x43, 0x45, 0x10, 0x13, 0x12, 0x13, 0x0a, 0x0f, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x5f, - 0x4b, 0x45, 0x59, 0x53, 0x45, 0x4e, 0x44, 0x10, 0x14, 0x12, 0x13, 0x0a, 0x0f, 0x4d, 0x50, 0x50, - 0x5f, 0x49, 0x4e, 0x5f, 0x50, 0x52, 0x4f, 0x47, 0x52, 0x45, 0x53, 0x53, 0x10, 0x15, 0x12, 0x12, - 0x0a, 0x0e, 0x43, 0x49, 0x52, 0x43, 0x55, 0x4c, 0x41, 0x52, 0x5f, 0x52, 0x4f, 0x55, 0x54, 0x45, - 0x10, 0x16, 0x2a, 0xae, 0x01, 0x0a, 0x0c, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x12, 0x0d, 0x0a, 0x09, 0x49, 0x4e, 0x5f, 0x46, 0x4c, 0x49, 0x47, 0x48, 0x54, - 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x53, 0x55, 0x43, 0x43, 0x45, 0x45, 0x44, 0x45, 0x44, 0x10, - 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x5f, 0x54, 0x49, 0x4d, 0x45, - 0x4f, 0x55, 0x54, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x5f, - 0x4e, 0x4f, 0x5f, 0x52, 0x4f, 0x55, 0x54, 0x45, 0x10, 0x03, 0x12, 0x10, 0x0a, 0x0c, 0x46, 0x41, - 0x49, 0x4c, 0x45, 0x44, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x12, 0x24, 0x0a, 0x20, - 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 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, 0x05, 0x12, 0x1f, 0x0a, 0x1b, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x5f, 0x49, 0x4e, 0x53, - 0x55, 0x46, 0x46, 0x49, 0x43, 0x49, 0x45, 0x4e, 0x54, 0x5f, 0x42, 0x41, 0x4c, 0x41, 0x4e, 0x43, - 0x45, 0x10, 0x06, 0x2a, 0x51, 0x0a, 0x18, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x48, 0x6f, - 0x6c, 0x64, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, - 0x0a, 0x0a, 0x06, 0x53, 0x45, 0x54, 0x54, 0x4c, 0x45, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x46, - 0x41, 0x49, 0x4c, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x52, 0x45, 0x53, 0x55, 0x4d, 0x45, 0x10, - 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x52, 0x45, 0x53, 0x55, 0x4d, 0x45, 0x5f, 0x4d, 0x4f, 0x44, 0x49, - 0x46, 0x49, 0x45, 0x44, 0x10, 0x03, 0x2a, 0x35, 0x0a, 0x10, 0x43, 0x68, 0x61, 0x6e, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x06, 0x45, 0x4e, - 0x41, 0x42, 0x4c, 0x45, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x49, 0x53, 0x41, 0x42, 0x4c, - 0x45, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x55, 0x54, 0x4f, 0x10, 0x02, 0x32, 0xc6, 0x0e, - 0x0a, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x12, 0x40, 0x0a, 0x0d, 0x53, 0x65, 0x6e, 0x64, - 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x56, 0x32, 0x12, 0x1d, 0x2e, 0x72, 0x6f, 0x75, 0x74, - 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, - 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, - 0x2e, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x0e, 0x54, 0x72, - 0x61, 0x63, 0x6b, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x56, 0x32, 0x12, 0x1e, 0x2e, 0x72, - 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x50, 0x61, - 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x6c, - 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x12, 0x42, - 0x0a, 0x0d, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, - 0x1f, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x72, 0x61, 0x63, - 0x6b, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x0e, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, - 0x30, 0x01, 0x12, 0x4b, 0x0a, 0x10, 0x45, 0x73, 0x74, 0x69, 0x6d, 0x61, 0x74, 0x65, 0x52, 0x6f, - 0x75, 0x74, 0x65, 0x46, 0x65, 0x65, 0x12, 0x1a, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, - 0x70, 0x63, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x46, 0x65, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x52, - 0x6f, 0x75, 0x74, 0x65, 0x46, 0x65, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x51, 0x0a, 0x0b, 0x53, 0x65, 0x6e, 0x64, 0x54, 0x6f, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x1d, - 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x54, - 0x6f, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, - 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x54, 0x6f, - 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x88, - 0x02, 0x01, 0x12, 0x42, 0x0a, 0x0d, 0x53, 0x65, 0x6e, 0x64, 0x54, 0x6f, 0x52, 0x6f, 0x75, 0x74, - 0x65, 0x56, 0x32, 0x12, 0x1d, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, - 0x53, 0x65, 0x6e, 0x64, 0x54, 0x6f, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x48, 0x54, 0x4c, 0x43, 0x41, - 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x12, 0x64, 0x0a, 0x13, 0x52, 0x65, 0x73, 0x65, 0x74, 0x4d, - 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x12, 0x25, 0x2e, - 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x73, 0x65, 0x74, 0x4d, - 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, - 0x2e, 0x52, 0x65, 0x73, 0x65, 0x74, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, - 0x74, 0x72, 0x6f, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x64, 0x0a, 0x13, - 0x51, 0x75, 0x65, 0x72, 0x79, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, + 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x62, 0x61, 0x73, 0x65, 0x22, 0x9a, + 0x01, 0x0a, 0x1e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 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, 0x2e, 0x0a, 0x12, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x62, 0x65, 0x66, 0x6f, + 0x72, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x48, 0x00, 0x52, + 0x10, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x42, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x54, 0x69, 0x6d, + 0x65, 0x12, 0x1c, 0x0a, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, + 0x1d, 0x0a, 0x0a, 0x62, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0d, 0x52, 0x09, 0x62, 0x61, 0x74, 0x63, 0x68, 0x53, 0x69, 0x7a, 0x65, 0x42, 0x0b, + 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x22, 0x86, 0x01, 0x0a, 0x1f, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 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, + 0x25, 0x0a, 0x0e, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x5f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x44, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, + 0x66, 0x65, 0x65, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, + 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x46, 0x65, 0x65, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x16, 0x0a, 0x06, + 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x2a, 0x81, 0x04, 0x0a, 0x0d, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, + 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, + 0x4e, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x4e, 0x4f, 0x5f, 0x44, 0x45, 0x54, 0x41, 0x49, 0x4c, + 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x4f, 0x4e, 0x49, 0x4f, 0x4e, 0x5f, 0x44, 0x45, 0x43, 0x4f, + 0x44, 0x45, 0x10, 0x02, 0x12, 0x15, 0x0a, 0x11, 0x4c, 0x49, 0x4e, 0x4b, 0x5f, 0x4e, 0x4f, 0x54, + 0x5f, 0x45, 0x4c, 0x49, 0x47, 0x49, 0x42, 0x4c, 0x45, 0x10, 0x03, 0x12, 0x14, 0x0a, 0x10, 0x4f, + 0x4e, 0x5f, 0x43, 0x48, 0x41, 0x49, 0x4e, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x10, + 0x04, 0x12, 0x14, 0x0a, 0x10, 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x45, 0x58, 0x43, 0x45, 0x45, 0x44, + 0x53, 0x5f, 0x4d, 0x41, 0x58, 0x10, 0x05, 0x12, 0x18, 0x0a, 0x14, 0x49, 0x4e, 0x53, 0x55, 0x46, + 0x46, 0x49, 0x43, 0x49, 0x45, 0x4e, 0x54, 0x5f, 0x42, 0x41, 0x4c, 0x41, 0x4e, 0x43, 0x45, 0x10, + 0x06, 0x12, 0x16, 0x0a, 0x12, 0x49, 0x4e, 0x43, 0x4f, 0x4d, 0x50, 0x4c, 0x45, 0x54, 0x45, 0x5f, + 0x46, 0x4f, 0x52, 0x57, 0x41, 0x52, 0x44, 0x10, 0x07, 0x12, 0x13, 0x0a, 0x0f, 0x48, 0x54, 0x4c, + 0x43, 0x5f, 0x41, 0x44, 0x44, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x08, 0x12, 0x15, + 0x0a, 0x11, 0x46, 0x4f, 0x52, 0x57, 0x41, 0x52, 0x44, 0x53, 0x5f, 0x44, 0x49, 0x53, 0x41, 0x42, + 0x4c, 0x45, 0x44, 0x10, 0x09, 0x12, 0x14, 0x0a, 0x10, 0x49, 0x4e, 0x56, 0x4f, 0x49, 0x43, 0x45, + 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x45, 0x44, 0x10, 0x0a, 0x12, 0x15, 0x0a, 0x11, 0x49, + 0x4e, 0x56, 0x4f, 0x49, 0x43, 0x45, 0x5f, 0x55, 0x4e, 0x44, 0x45, 0x52, 0x50, 0x41, 0x49, 0x44, + 0x10, 0x0b, 0x12, 0x1b, 0x0a, 0x17, 0x49, 0x4e, 0x56, 0x4f, 0x49, 0x43, 0x45, 0x5f, 0x45, 0x58, + 0x50, 0x49, 0x52, 0x59, 0x5f, 0x54, 0x4f, 0x4f, 0x5f, 0x53, 0x4f, 0x4f, 0x4e, 0x10, 0x0c, 0x12, + 0x14, 0x0a, 0x10, 0x49, 0x4e, 0x56, 0x4f, 0x49, 0x43, 0x45, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x4f, + 0x50, 0x45, 0x4e, 0x10, 0x0d, 0x12, 0x17, 0x0a, 0x13, 0x4d, 0x50, 0x50, 0x5f, 0x49, 0x4e, 0x56, + 0x4f, 0x49, 0x43, 0x45, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x10, 0x0e, 0x12, 0x14, + 0x0a, 0x10, 0x41, 0x44, 0x44, 0x52, 0x45, 0x53, 0x53, 0x5f, 0x4d, 0x49, 0x53, 0x4d, 0x41, 0x54, + 0x43, 0x48, 0x10, 0x0f, 0x12, 0x16, 0x0a, 0x12, 0x53, 0x45, 0x54, 0x5f, 0x54, 0x4f, 0x54, 0x41, + 0x4c, 0x5f, 0x4d, 0x49, 0x53, 0x4d, 0x41, 0x54, 0x43, 0x48, 0x10, 0x10, 0x12, 0x15, 0x0a, 0x11, + 0x53, 0x45, 0x54, 0x5f, 0x54, 0x4f, 0x54, 0x41, 0x4c, 0x5f, 0x54, 0x4f, 0x4f, 0x5f, 0x4c, 0x4f, + 0x57, 0x10, 0x11, 0x12, 0x10, 0x0a, 0x0c, 0x53, 0x45, 0x54, 0x5f, 0x4f, 0x56, 0x45, 0x52, 0x50, + 0x41, 0x49, 0x44, 0x10, 0x12, 0x12, 0x13, 0x0a, 0x0f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, + 0x5f, 0x49, 0x4e, 0x56, 0x4f, 0x49, 0x43, 0x45, 0x10, 0x13, 0x12, 0x13, 0x0a, 0x0f, 0x49, 0x4e, + 0x56, 0x41, 0x4c, 0x49, 0x44, 0x5f, 0x4b, 0x45, 0x59, 0x53, 0x45, 0x4e, 0x44, 0x10, 0x14, 0x12, + 0x13, 0x0a, 0x0f, 0x4d, 0x50, 0x50, 0x5f, 0x49, 0x4e, 0x5f, 0x50, 0x52, 0x4f, 0x47, 0x52, 0x45, + 0x53, 0x53, 0x10, 0x15, 0x12, 0x12, 0x0a, 0x0e, 0x43, 0x49, 0x52, 0x43, 0x55, 0x4c, 0x41, 0x52, + 0x5f, 0x52, 0x4f, 0x55, 0x54, 0x45, 0x10, 0x16, 0x2a, 0xae, 0x01, 0x0a, 0x0c, 0x50, 0x61, 0x79, + 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0d, 0x0a, 0x09, 0x49, 0x4e, 0x5f, + 0x46, 0x4c, 0x49, 0x47, 0x48, 0x54, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x53, 0x55, 0x43, 0x43, + 0x45, 0x45, 0x44, 0x45, 0x44, 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x46, 0x41, 0x49, 0x4c, 0x45, + 0x44, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x46, + 0x41, 0x49, 0x4c, 0x45, 0x44, 0x5f, 0x4e, 0x4f, 0x5f, 0x52, 0x4f, 0x55, 0x54, 0x45, 0x10, 0x03, + 0x12, 0x10, 0x0a, 0x0c, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, + 0x10, 0x04, 0x12, 0x24, 0x0a, 0x20, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 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, 0x05, 0x12, 0x1f, 0x0a, 0x1b, 0x46, 0x41, 0x49, 0x4c, + 0x45, 0x44, 0x5f, 0x49, 0x4e, 0x53, 0x55, 0x46, 0x46, 0x49, 0x43, 0x49, 0x45, 0x4e, 0x54, 0x5f, + 0x42, 0x41, 0x4c, 0x41, 0x4e, 0x43, 0x45, 0x10, 0x06, 0x2a, 0x51, 0x0a, 0x18, 0x52, 0x65, 0x73, + 0x6f, 0x6c, 0x76, 0x65, 0x48, 0x6f, 0x6c, 0x64, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x41, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x45, 0x54, 0x54, 0x4c, 0x45, 0x10, + 0x00, 0x12, 0x08, 0x0a, 0x04, 0x46, 0x41, 0x49, 0x4c, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x52, + 0x45, 0x53, 0x55, 0x4d, 0x45, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x52, 0x45, 0x53, 0x55, 0x4d, + 0x45, 0x5f, 0x4d, 0x4f, 0x44, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x03, 0x2a, 0x35, 0x0a, 0x10, + 0x43, 0x68, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0x0a, 0x0a, 0x06, 0x45, 0x4e, 0x41, 0x42, 0x4c, 0x45, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, + 0x44, 0x49, 0x53, 0x41, 0x42, 0x4c, 0x45, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x55, 0x54, + 0x4f, 0x10, 0x02, 0x32, 0xb8, 0x0f, 0x0a, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x12, 0x40, + 0x0a, 0x0d, 0x53, 0x65, 0x6e, 0x64, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x56, 0x32, 0x12, + 0x1d, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, + 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0e, + 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x30, 0x01, + 0x12, 0x42, 0x0a, 0x0e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, + 0x56, 0x32, 0x12, 0x1e, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x54, + 0x72, 0x61, 0x63, 0x6b, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x61, 0x79, 0x6d, 0x65, + 0x6e, 0x74, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x0d, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x50, 0x61, 0x79, + 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x1f, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, + 0x63, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, + 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x12, 0x4b, 0x0a, 0x10, 0x45, 0x73, 0x74, 0x69, + 0x6d, 0x61, 0x74, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x46, 0x65, 0x65, 0x12, 0x1a, 0x2e, 0x72, + 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x46, 0x65, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, + 0x72, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x46, 0x65, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x51, 0x0a, 0x0b, 0x53, 0x65, 0x6e, 0x64, 0x54, 0x6f, 0x52, + 0x6f, 0x75, 0x74, 0x65, 0x12, 0x1d, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, + 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x54, 0x6f, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, + 0x53, 0x65, 0x6e, 0x64, 0x54, 0x6f, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x03, 0x88, 0x02, 0x01, 0x12, 0x42, 0x0a, 0x0d, 0x53, 0x65, 0x6e, 0x64, + 0x54, 0x6f, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x56, 0x32, 0x12, 0x1d, 0x2e, 0x72, 0x6f, 0x75, 0x74, + 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x54, 0x6f, 0x52, 0x6f, 0x75, 0x74, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, + 0x2e, 0x48, 0x54, 0x4c, 0x43, 0x41, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x12, 0x64, 0x0a, 0x13, + 0x52, 0x65, 0x73, 0x65, 0x74, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x12, 0x25, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, - 0x51, 0x75, 0x65, 0x72, 0x79, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, + 0x52, 0x65, 0x73, 0x65, 0x74, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x72, 0x6f, 0x75, - 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x4d, 0x69, 0x73, 0x73, + 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x73, 0x65, 0x74, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x6a, 0x0a, 0x15, 0x58, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x4d, 0x69, 0x73, - 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x12, 0x27, 0x2e, 0x72, 0x6f, - 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x58, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x4d, - 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, - 0x2e, 0x58, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, - 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x70, - 0x0a, 0x17, 0x47, 0x65, 0x74, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, - 0x72, 0x6f, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x29, 0x2e, 0x72, 0x6f, 0x75, 0x74, - 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, - 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, - 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, - 0x6f, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x70, 0x0a, 0x17, 0x53, 0x65, 0x74, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, - 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x29, 0x2e, 0x72, 0x6f, - 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x74, 0x4d, 0x69, 0x73, 0x73, 0x69, - 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, - 0x70, 0x63, 0x2e, 0x53, 0x65, 0x74, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, - 0x74, 0x72, 0x6f, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x5b, 0x0a, 0x10, 0x51, 0x75, 0x65, 0x72, 0x79, 0x50, 0x72, 0x6f, 0x62, 0x61, - 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x12, 0x22, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, - 0x70, 0x63, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x50, 0x72, 0x6f, 0x62, 0x61, 0x62, 0x69, 0x6c, - 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x72, 0x6f, 0x75, - 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x50, 0x72, 0x6f, 0x62, - 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x49, 0x0a, 0x0a, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x1c, 0x2e, - 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x52, - 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x72, 0x6f, - 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x52, 0x6f, 0x75, - 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x54, 0x0a, 0x13, 0x53, 0x75, - 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x48, 0x74, 0x6c, 0x63, 0x45, 0x76, 0x65, 0x6e, 0x74, - 0x73, 0x12, 0x25, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x75, - 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x48, 0x74, 0x6c, 0x63, 0x45, 0x76, 0x65, 0x6e, 0x74, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, - 0x72, 0x72, 0x70, 0x63, 0x2e, 0x48, 0x74, 0x6c, 0x63, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, - 0x12, 0x4d, 0x0a, 0x0b, 0x53, 0x65, 0x6e, 0x64, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x12, - 0x1d, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, - 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, - 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x61, 0x79, 0x6d, 0x65, - 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x03, 0x88, 0x02, 0x01, 0x30, 0x01, 0x12, - 0x4f, 0x0a, 0x0c, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x12, - 0x1e, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x72, 0x61, 0x63, - 0x6b, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x18, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x61, 0x79, 0x6d, - 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x03, 0x88, 0x02, 0x01, 0x30, 0x01, - 0x12, 0x66, 0x0a, 0x0f, 0x48, 0x74, 0x6c, 0x63, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, - 0x74, 0x6f, 0x72, 0x12, 0x27, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, - 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x48, 0x74, 0x6c, 0x63, 0x49, 0x6e, 0x74, 0x65, 0x72, - 0x63, 0x65, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x1a, 0x26, 0x2e, 0x72, - 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, - 0x48, 0x74, 0x6c, 0x63, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x28, 0x01, 0x30, 0x01, 0x12, 0x5b, 0x0a, 0x10, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x22, 0x2e, 0x72, - 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, - 0x68, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x23, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x14, 0x58, 0x41, 0x64, 0x64, 0x4c, 0x6f, 0x63, - 0x61, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x73, 0x12, 0x1c, 0x2e, - 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x41, 0x6c, 0x69, - 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x72, 0x6f, - 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x41, 0x6c, 0x69, 0x61, 0x73, - 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5c, 0x0a, 0x17, 0x58, 0x44, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x41, 0x6c, - 0x69, 0x61, 0x73, 0x65, 0x73, 0x12, 0x1f, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, - 0x63, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, - 0x70, 0x63, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5c, 0x0a, 0x17, 0x58, 0x46, 0x69, 0x6e, - 0x64, 0x42, 0x61, 0x73, 0x65, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x41, 0x6c, - 0x69, 0x61, 0x73, 0x12, 0x1f, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, - 0x46, 0x69, 0x6e, 0x64, 0x42, 0x61, 0x73, 0x65, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, - 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x42, 0x61, 0x73, 0x65, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x31, 0x5a, 0x2f, 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, 0x2f, - 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x33, + 0x73, 0x65, 0x12, 0x64, 0x0a, 0x13, 0x51, 0x75, 0x65, 0x72, 0x79, 0x4d, 0x69, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x12, 0x25, 0x2e, 0x72, 0x6f, 0x75, 0x74, + 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x4d, 0x69, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x26, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x65, + 0x72, 0x79, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6a, 0x0a, 0x15, 0x58, 0x49, 0x6d, 0x70, + 0x6f, 0x72, 0x74, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, + 0x6c, 0x12, 0x27, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x58, 0x49, + 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, + 0x72, 0x6f, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x72, 0x6f, 0x75, + 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x58, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x4d, 0x69, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x70, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x4d, 0x69, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, + 0x29, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x47, 0x65, 0x74, 0x4d, + 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x72, 0x6f, 0x75, + 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x70, 0x0a, 0x17, 0x53, 0x65, 0x74, 0x4d, 0x69, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x12, 0x29, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, + 0x74, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x72, + 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x74, 0x4d, 0x69, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5b, 0x0a, 0x10, 0x51, 0x75, 0x65, 0x72, + 0x79, 0x50, 0x72, 0x6f, 0x62, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x12, 0x22, 0x2e, 0x72, + 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x50, 0x72, + 0x6f, 0x62, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x23, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x65, + 0x72, 0x79, 0x50, 0x72, 0x6f, 0x62, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, 0x0a, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x52, 0x6f, + 0x75, 0x74, 0x65, 0x12, 0x1c, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, + 0x42, 0x75, 0x69, 0x6c, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x1d, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x42, 0x75, + 0x69, 0x6c, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x54, 0x0a, 0x13, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x48, 0x74, 0x6c, + 0x63, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x25, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, + 0x72, 0x70, 0x63, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x48, 0x74, 0x6c, + 0x63, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, + 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x48, 0x74, 0x6c, 0x63, 0x45, + 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x12, 0x4d, 0x0a, 0x0b, 0x53, 0x65, 0x6e, 0x64, 0x50, 0x61, + 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x1d, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, + 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, + 0x2e, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x03, + 0x88, 0x02, 0x01, 0x30, 0x01, 0x12, 0x4f, 0x0a, 0x0c, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x50, 0x61, + 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x1e, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, + 0x63, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, + 0x63, 0x2e, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, + 0x03, 0x88, 0x02, 0x01, 0x30, 0x01, 0x12, 0x66, 0x0a, 0x0f, 0x48, 0x74, 0x6c, 0x63, 0x49, 0x6e, + 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x6f, 0x72, 0x12, 0x27, 0x2e, 0x72, 0x6f, 0x75, 0x74, + 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x48, 0x74, 0x6c, + 0x63, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x1a, 0x26, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x46, + 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x48, 0x74, 0x6c, 0x63, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, + 0x65, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x28, 0x01, 0x30, 0x01, 0x12, 0x5b, + 0x0a, 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x12, 0x22, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, + 0x70, 0x63, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x53, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x14, 0x58, + 0x41, 0x64, 0x64, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x41, 0x6c, 0x69, 0x61, + 0x73, 0x65, 0x73, 0x12, 0x1c, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, + 0x41, 0x64, 0x64, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x1d, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, + 0x64, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x5c, 0x0a, 0x17, 0x58, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4c, 0x6f, 0x63, 0x61, 0x6c, + 0x43, 0x68, 0x61, 0x6e, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x73, 0x12, 0x1f, 0x2e, 0x72, 0x6f, + 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x6c, + 0x69, 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72, + 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, + 0x6c, 0x69, 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5c, + 0x0a, 0x17, 0x58, 0x46, 0x69, 0x6e, 0x64, 0x42, 0x61, 0x73, 0x65, 0x4c, 0x6f, 0x63, 0x61, 0x6c, + 0x43, 0x68, 0x61, 0x6e, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x12, 0x1f, 0x2e, 0x72, 0x6f, 0x75, 0x74, + 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x42, 0x61, 0x73, 0x65, 0x41, 0x6c, + 0x69, 0x61, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72, 0x6f, 0x75, + 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x42, 0x61, 0x73, 0x65, 0x41, + 0x6c, 0x69, 0x61, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x70, 0x0a, 0x17, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, + 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x29, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, + 0x72, 0x70, 0x63, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 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, 0x2a, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x44, + 0x65, 0x6c, 0x65, 0x74, 0x65, 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, 0x42, 0x31, + 0x5a, 0x2f, 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, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, + 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -4427,7 +4620,7 @@ func file_routerrpc_router_proto_rawDescGZIP() []byte { } var file_routerrpc_router_proto_enumTypes = make([]protoimpl.EnumInfo, 6) -var file_routerrpc_router_proto_msgTypes = make([]protoimpl.MessageInfo, 54) +var file_routerrpc_router_proto_msgTypes = make([]protoimpl.MessageInfo, 56) var file_routerrpc_router_proto_goTypes = []interface{}{ (FailureDetail)(0), // 0: routerrpc.FailureDetail (PaymentState)(0), // 1: routerrpc.PaymentState @@ -4482,33 +4675,35 @@ var file_routerrpc_router_proto_goTypes = []interface{}{ (*DeleteAliasesResponse)(nil), // 50: routerrpc.DeleteAliasesResponse (*FindBaseAliasRequest)(nil), // 51: routerrpc.FindBaseAliasRequest (*FindBaseAliasResponse)(nil), // 52: routerrpc.FindBaseAliasResponse - nil, // 53: routerrpc.SendPaymentRequest.DestCustomRecordsEntry - nil, // 54: routerrpc.SendPaymentRequest.FirstHopCustomRecordsEntry - nil, // 55: routerrpc.SendToRouteRequest.FirstHopCustomRecordsEntry - nil, // 56: routerrpc.BuildRouteRequest.FirstHopCustomRecordsEntry - nil, // 57: routerrpc.ForwardHtlcInterceptRequest.CustomRecordsEntry - nil, // 58: routerrpc.ForwardHtlcInterceptRequest.InWireCustomRecordsEntry - nil, // 59: routerrpc.ForwardHtlcInterceptResponse.OutWireCustomRecordsEntry - (*lnrpc.RouteHint)(nil), // 60: lnrpc.RouteHint - (lnrpc.FeatureBit)(0), // 61: lnrpc.FeatureBit - (lnrpc.PaymentFailureReason)(0), // 62: lnrpc.PaymentFailureReason - (*lnrpc.Route)(nil), // 63: lnrpc.Route - (*lnrpc.Failure)(nil), // 64: lnrpc.Failure - (lnrpc.Failure_FailureCode)(0), // 65: lnrpc.Failure.FailureCode - (*lnrpc.HTLCAttempt)(nil), // 66: lnrpc.HTLCAttempt - (*lnrpc.ChannelPoint)(nil), // 67: lnrpc.ChannelPoint - (*lnrpc.AliasMap)(nil), // 68: lnrpc.AliasMap - (*lnrpc.Payment)(nil), // 69: lnrpc.Payment + (*DeleteForwardingHistoryRequest)(nil), // 53: routerrpc.DeleteForwardingHistoryRequest + (*DeleteForwardingHistoryResponse)(nil), // 54: routerrpc.DeleteForwardingHistoryResponse + nil, // 55: routerrpc.SendPaymentRequest.DestCustomRecordsEntry + nil, // 56: routerrpc.SendPaymentRequest.FirstHopCustomRecordsEntry + nil, // 57: routerrpc.SendToRouteRequest.FirstHopCustomRecordsEntry + nil, // 58: routerrpc.BuildRouteRequest.FirstHopCustomRecordsEntry + nil, // 59: routerrpc.ForwardHtlcInterceptRequest.CustomRecordsEntry + nil, // 60: routerrpc.ForwardHtlcInterceptRequest.InWireCustomRecordsEntry + nil, // 61: routerrpc.ForwardHtlcInterceptResponse.OutWireCustomRecordsEntry + (*lnrpc.RouteHint)(nil), // 62: lnrpc.RouteHint + (lnrpc.FeatureBit)(0), // 63: lnrpc.FeatureBit + (lnrpc.PaymentFailureReason)(0), // 64: lnrpc.PaymentFailureReason + (*lnrpc.Route)(nil), // 65: lnrpc.Route + (*lnrpc.Failure)(nil), // 66: lnrpc.Failure + (lnrpc.Failure_FailureCode)(0), // 67: lnrpc.Failure.FailureCode + (*lnrpc.HTLCAttempt)(nil), // 68: lnrpc.HTLCAttempt + (*lnrpc.ChannelPoint)(nil), // 69: lnrpc.ChannelPoint + (*lnrpc.AliasMap)(nil), // 70: lnrpc.AliasMap + (*lnrpc.Payment)(nil), // 71: lnrpc.Payment } var file_routerrpc_router_proto_depIdxs = []int32{ - 60, // 0: routerrpc.SendPaymentRequest.route_hints:type_name -> lnrpc.RouteHint - 53, // 1: routerrpc.SendPaymentRequest.dest_custom_records:type_name -> routerrpc.SendPaymentRequest.DestCustomRecordsEntry - 61, // 2: routerrpc.SendPaymentRequest.dest_features:type_name -> lnrpc.FeatureBit - 54, // 3: routerrpc.SendPaymentRequest.first_hop_custom_records:type_name -> routerrpc.SendPaymentRequest.FirstHopCustomRecordsEntry - 62, // 4: routerrpc.RouteFeeResponse.failure_reason:type_name -> lnrpc.PaymentFailureReason - 63, // 5: routerrpc.SendToRouteRequest.route:type_name -> lnrpc.Route - 55, // 6: routerrpc.SendToRouteRequest.first_hop_custom_records:type_name -> routerrpc.SendToRouteRequest.FirstHopCustomRecordsEntry - 64, // 7: routerrpc.SendToRouteResponse.failure:type_name -> lnrpc.Failure + 62, // 0: routerrpc.SendPaymentRequest.route_hints:type_name -> lnrpc.RouteHint + 55, // 1: routerrpc.SendPaymentRequest.dest_custom_records:type_name -> routerrpc.SendPaymentRequest.DestCustomRecordsEntry + 63, // 2: routerrpc.SendPaymentRequest.dest_features:type_name -> lnrpc.FeatureBit + 56, // 3: routerrpc.SendPaymentRequest.first_hop_custom_records:type_name -> routerrpc.SendPaymentRequest.FirstHopCustomRecordsEntry + 64, // 4: routerrpc.RouteFeeResponse.failure_reason:type_name -> lnrpc.PaymentFailureReason + 65, // 5: routerrpc.SendToRouteRequest.route:type_name -> lnrpc.Route + 57, // 6: routerrpc.SendToRouteRequest.first_hop_custom_records:type_name -> routerrpc.SendToRouteRequest.FirstHopCustomRecordsEntry + 66, // 7: routerrpc.SendToRouteResponse.failure:type_name -> lnrpc.Failure 19, // 8: routerrpc.QueryMissionControlResponse.pairs:type_name -> routerrpc.PairHistory 19, // 9: routerrpc.XImportMissionControlRequest.pairs:type_name -> routerrpc.PairHistory 20, // 10: routerrpc.PairHistory.history:type_name -> routerrpc.PairData @@ -4518,8 +4713,8 @@ var file_routerrpc_router_proto_depIdxs = []int32{ 27, // 14: routerrpc.MissionControlConfig.apriori:type_name -> routerrpc.AprioriParameters 26, // 15: routerrpc.MissionControlConfig.bimodal:type_name -> routerrpc.BimodalParameters 20, // 16: routerrpc.QueryProbabilityResponse.history:type_name -> routerrpc.PairData - 56, // 17: routerrpc.BuildRouteRequest.first_hop_custom_records:type_name -> routerrpc.BuildRouteRequest.FirstHopCustomRecordsEntry - 63, // 18: routerrpc.BuildRouteResponse.route:type_name -> lnrpc.Route + 58, // 17: routerrpc.BuildRouteRequest.first_hop_custom_records:type_name -> routerrpc.BuildRouteRequest.FirstHopCustomRecordsEntry + 65, // 18: routerrpc.BuildRouteResponse.route:type_name -> lnrpc.Route 5, // 19: routerrpc.HtlcEvent.event_type:type_name -> routerrpc.HtlcEvent.EventType 35, // 20: routerrpc.HtlcEvent.forward_event:type_name -> routerrpc.ForwardEvent 36, // 21: routerrpc.HtlcEvent.forward_fail_event:type_name -> routerrpc.ForwardFailEvent @@ -4529,23 +4724,23 @@ var file_routerrpc_router_proto_depIdxs = []int32{ 38, // 25: routerrpc.HtlcEvent.final_htlc_event:type_name -> routerrpc.FinalHtlcEvent 34, // 26: routerrpc.ForwardEvent.info:type_name -> routerrpc.HtlcInfo 34, // 27: routerrpc.LinkFailEvent.info:type_name -> routerrpc.HtlcInfo - 65, // 28: routerrpc.LinkFailEvent.wire_failure:type_name -> lnrpc.Failure.FailureCode + 67, // 28: routerrpc.LinkFailEvent.wire_failure:type_name -> lnrpc.Failure.FailureCode 0, // 29: routerrpc.LinkFailEvent.failure_detail:type_name -> routerrpc.FailureDetail 1, // 30: routerrpc.PaymentStatus.state:type_name -> routerrpc.PaymentState - 66, // 31: routerrpc.PaymentStatus.htlcs:type_name -> lnrpc.HTLCAttempt + 68, // 31: routerrpc.PaymentStatus.htlcs:type_name -> lnrpc.HTLCAttempt 42, // 32: routerrpc.ForwardHtlcInterceptRequest.incoming_circuit_key:type_name -> routerrpc.CircuitKey - 57, // 33: routerrpc.ForwardHtlcInterceptRequest.custom_records:type_name -> routerrpc.ForwardHtlcInterceptRequest.CustomRecordsEntry - 58, // 34: routerrpc.ForwardHtlcInterceptRequest.in_wire_custom_records:type_name -> routerrpc.ForwardHtlcInterceptRequest.InWireCustomRecordsEntry + 59, // 33: routerrpc.ForwardHtlcInterceptRequest.custom_records:type_name -> routerrpc.ForwardHtlcInterceptRequest.CustomRecordsEntry + 60, // 34: routerrpc.ForwardHtlcInterceptRequest.in_wire_custom_records:type_name -> routerrpc.ForwardHtlcInterceptRequest.InWireCustomRecordsEntry 42, // 35: routerrpc.ForwardHtlcInterceptResponse.incoming_circuit_key:type_name -> routerrpc.CircuitKey 2, // 36: routerrpc.ForwardHtlcInterceptResponse.action:type_name -> routerrpc.ResolveHoldForwardAction - 65, // 37: routerrpc.ForwardHtlcInterceptResponse.failure_code:type_name -> lnrpc.Failure.FailureCode - 59, // 38: routerrpc.ForwardHtlcInterceptResponse.out_wire_custom_records:type_name -> routerrpc.ForwardHtlcInterceptResponse.OutWireCustomRecordsEntry - 67, // 39: routerrpc.UpdateChanStatusRequest.chan_point:type_name -> lnrpc.ChannelPoint + 67, // 37: routerrpc.ForwardHtlcInterceptResponse.failure_code:type_name -> lnrpc.Failure.FailureCode + 61, // 38: routerrpc.ForwardHtlcInterceptResponse.out_wire_custom_records:type_name -> routerrpc.ForwardHtlcInterceptResponse.OutWireCustomRecordsEntry + 69, // 39: routerrpc.UpdateChanStatusRequest.chan_point:type_name -> lnrpc.ChannelPoint 3, // 40: routerrpc.UpdateChanStatusRequest.action:type_name -> routerrpc.ChanStatusAction - 68, // 41: routerrpc.AddAliasesRequest.alias_maps:type_name -> lnrpc.AliasMap - 68, // 42: routerrpc.AddAliasesResponse.alias_maps:type_name -> lnrpc.AliasMap - 68, // 43: routerrpc.DeleteAliasesRequest.alias_maps:type_name -> lnrpc.AliasMap - 68, // 44: routerrpc.DeleteAliasesResponse.alias_maps:type_name -> lnrpc.AliasMap + 70, // 41: routerrpc.AddAliasesRequest.alias_maps:type_name -> lnrpc.AliasMap + 70, // 42: routerrpc.AddAliasesResponse.alias_maps:type_name -> lnrpc.AliasMap + 70, // 43: routerrpc.DeleteAliasesRequest.alias_maps:type_name -> lnrpc.AliasMap + 70, // 44: routerrpc.DeleteAliasesResponse.alias_maps:type_name -> lnrpc.AliasMap 6, // 45: routerrpc.Router.SendPaymentV2:input_type -> routerrpc.SendPaymentRequest 7, // 46: routerrpc.Router.TrackPaymentV2:input_type -> routerrpc.TrackPaymentRequest 8, // 47: routerrpc.Router.TrackPayments:input_type -> routerrpc.TrackPaymentsRequest @@ -4567,29 +4762,31 @@ var file_routerrpc_router_proto_depIdxs = []int32{ 47, // 63: routerrpc.Router.XAddLocalChanAliases:input_type -> routerrpc.AddAliasesRequest 49, // 64: routerrpc.Router.XDeleteLocalChanAliases:input_type -> routerrpc.DeleteAliasesRequest 51, // 65: routerrpc.Router.XFindBaseLocalChanAlias:input_type -> routerrpc.FindBaseAliasRequest - 69, // 66: routerrpc.Router.SendPaymentV2:output_type -> lnrpc.Payment - 69, // 67: routerrpc.Router.TrackPaymentV2:output_type -> lnrpc.Payment - 69, // 68: routerrpc.Router.TrackPayments:output_type -> lnrpc.Payment - 10, // 69: routerrpc.Router.EstimateRouteFee:output_type -> routerrpc.RouteFeeResponse - 12, // 70: routerrpc.Router.SendToRoute:output_type -> routerrpc.SendToRouteResponse - 66, // 71: routerrpc.Router.SendToRouteV2:output_type -> lnrpc.HTLCAttempt - 14, // 72: routerrpc.Router.ResetMissionControl:output_type -> routerrpc.ResetMissionControlResponse - 16, // 73: routerrpc.Router.QueryMissionControl:output_type -> routerrpc.QueryMissionControlResponse - 18, // 74: routerrpc.Router.XImportMissionControl:output_type -> routerrpc.XImportMissionControlResponse - 22, // 75: routerrpc.Router.GetMissionControlConfig:output_type -> routerrpc.GetMissionControlConfigResponse - 24, // 76: routerrpc.Router.SetMissionControlConfig:output_type -> routerrpc.SetMissionControlConfigResponse - 29, // 77: routerrpc.Router.QueryProbability:output_type -> routerrpc.QueryProbabilityResponse - 31, // 78: routerrpc.Router.BuildRoute:output_type -> routerrpc.BuildRouteResponse - 33, // 79: routerrpc.Router.SubscribeHtlcEvents:output_type -> routerrpc.HtlcEvent - 41, // 80: routerrpc.Router.SendPayment:output_type -> routerrpc.PaymentStatus - 41, // 81: routerrpc.Router.TrackPayment:output_type -> routerrpc.PaymentStatus - 43, // 82: routerrpc.Router.HtlcInterceptor:output_type -> routerrpc.ForwardHtlcInterceptRequest - 46, // 83: routerrpc.Router.UpdateChanStatus:output_type -> routerrpc.UpdateChanStatusResponse - 48, // 84: routerrpc.Router.XAddLocalChanAliases:output_type -> routerrpc.AddAliasesResponse - 50, // 85: routerrpc.Router.XDeleteLocalChanAliases:output_type -> routerrpc.DeleteAliasesResponse - 52, // 86: routerrpc.Router.XFindBaseLocalChanAlias:output_type -> routerrpc.FindBaseAliasResponse - 66, // [66:87] is the sub-list for method output_type - 45, // [45:66] is the sub-list for method input_type + 53, // 66: routerrpc.Router.DeleteForwardingHistory:input_type -> routerrpc.DeleteForwardingHistoryRequest + 71, // 67: routerrpc.Router.SendPaymentV2:output_type -> lnrpc.Payment + 71, // 68: routerrpc.Router.TrackPaymentV2:output_type -> lnrpc.Payment + 71, // 69: routerrpc.Router.TrackPayments:output_type -> lnrpc.Payment + 10, // 70: routerrpc.Router.EstimateRouteFee:output_type -> routerrpc.RouteFeeResponse + 12, // 71: routerrpc.Router.SendToRoute:output_type -> routerrpc.SendToRouteResponse + 68, // 72: routerrpc.Router.SendToRouteV2:output_type -> lnrpc.HTLCAttempt + 14, // 73: routerrpc.Router.ResetMissionControl:output_type -> routerrpc.ResetMissionControlResponse + 16, // 74: routerrpc.Router.QueryMissionControl:output_type -> routerrpc.QueryMissionControlResponse + 18, // 75: routerrpc.Router.XImportMissionControl:output_type -> routerrpc.XImportMissionControlResponse + 22, // 76: routerrpc.Router.GetMissionControlConfig:output_type -> routerrpc.GetMissionControlConfigResponse + 24, // 77: routerrpc.Router.SetMissionControlConfig:output_type -> routerrpc.SetMissionControlConfigResponse + 29, // 78: routerrpc.Router.QueryProbability:output_type -> routerrpc.QueryProbabilityResponse + 31, // 79: routerrpc.Router.BuildRoute:output_type -> routerrpc.BuildRouteResponse + 33, // 80: routerrpc.Router.SubscribeHtlcEvents:output_type -> routerrpc.HtlcEvent + 41, // 81: routerrpc.Router.SendPayment:output_type -> routerrpc.PaymentStatus + 41, // 82: routerrpc.Router.TrackPayment:output_type -> routerrpc.PaymentStatus + 43, // 83: routerrpc.Router.HtlcInterceptor:output_type -> routerrpc.ForwardHtlcInterceptRequest + 46, // 84: routerrpc.Router.UpdateChanStatus:output_type -> routerrpc.UpdateChanStatusResponse + 48, // 85: routerrpc.Router.XAddLocalChanAliases:output_type -> routerrpc.AddAliasesResponse + 50, // 86: routerrpc.Router.XDeleteLocalChanAliases:output_type -> routerrpc.DeleteAliasesResponse + 52, // 87: routerrpc.Router.XFindBaseLocalChanAlias:output_type -> routerrpc.FindBaseAliasResponse + 54, // 88: routerrpc.Router.DeleteForwardingHistory:output_type -> routerrpc.DeleteForwardingHistoryResponse + 67, // [67:89] is the sub-list for method output_type + 45, // [45:67] is the sub-list for method input_type 45, // [45:45] is the sub-list for extension type_name 45, // [45:45] is the sub-list for extension extendee 0, // [0:45] is the sub-list for field type_name @@ -5165,6 +5362,30 @@ func file_routerrpc_router_proto_init() { return nil } } + file_routerrpc_router_proto_msgTypes[47].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DeleteForwardingHistoryRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_routerrpc_router_proto_msgTypes[48].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DeleteForwardingHistoryResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } file_routerrpc_router_proto_msgTypes[19].OneofWrappers = []interface{}{ (*MissionControlConfig_Apriori)(nil), @@ -5178,13 +5399,17 @@ func file_routerrpc_router_proto_init() { (*HtlcEvent_SubscribedEvent)(nil), (*HtlcEvent_FinalHtlcEvent)(nil), } + file_routerrpc_router_proto_msgTypes[47].OneofWrappers = []interface{}{ + (*DeleteForwardingHistoryRequest_DeleteBeforeTime)(nil), + (*DeleteForwardingHistoryRequest_Duration)(nil), + } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_routerrpc_router_proto_rawDesc, NumEnums: 6, - NumMessages: 54, + NumMessages: 56, NumExtensions: 0, NumServices: 1, }, diff --git a/lnrpc/routerrpc/router.pb.json.go b/lnrpc/routerrpc/router.pb.json.go index 18fbc07fa85..b5212b37344 100644 --- a/lnrpc/routerrpc/router.pb.json.go +++ b/lnrpc/routerrpc/router.pb.json.go @@ -622,4 +622,29 @@ func RegisterRouterJSONCallbacks(registry map[string]func(ctx context.Context, } callback(string(respBytes), nil) } + + registry["routerrpc.Router.DeleteForwardingHistory"] = func(ctx context.Context, + conn *grpc.ClientConn, reqJSON string, callback func(string, error)) { + + req := &DeleteForwardingHistoryRequest{} + err := marshaler.Unmarshal([]byte(reqJSON), req) + if err != nil { + callback("", err) + return + } + + client := NewRouterClient(conn) + resp, err := client.DeleteForwardingHistory(ctx, req) + if err != nil { + callback("", err) + return + } + + respBytes, err := marshaler.Marshal(resp) + if err != nil { + callback("", err) + return + } + callback(string(respBytes), nil) + } } diff --git a/lnrpc/routerrpc/router.proto b/lnrpc/routerrpc/router.proto index 9e305e37e64..2522106b199 100644 --- a/lnrpc/routerrpc/router.proto +++ b/lnrpc/routerrpc/router.proto @@ -202,6 +202,18 @@ service Router { */ rpc XFindBaseLocalChanAlias (FindBaseAliasRequest) returns (FindBaseAliasResponse); + + /* lncli: `deletefwdhistory` + DeleteForwardingHistory allows the caller to delete forwarding history + events older than a specified time. This is useful for implementing data + retention policies for privacy purposes. The call deletes events in batches + and returns statistics including the total number of events deleted and the + aggregate fees earned from those events. The deletion is performed in a + transaction-safe manner with configurable batch sizes to avoid holding + large database locks. + */ + rpc DeleteForwardingHistory (DeleteForwardingHistoryRequest) + returns (DeleteForwardingHistoryResponse); } message SendPaymentRequest { @@ -1133,4 +1145,40 @@ message FindBaseAliasRequest { message FindBaseAliasResponse { // The base scid that resulted from the base scid look up. uint64 base = 1; +} + +message DeleteForwardingHistoryRequest { + // Specify the time before which to delete forwarding events using one of + // the following options: + oneof time_spec { + // Absolute Unix timestamp (seconds) - delete events before this time. + uint64 delete_before_time = 1; + + // Relative duration string supporting both standard Go format and + // custom units for convenience. + // Standard Go: "-24h" for 1 day, "-1.5h" for 1.5 hours + // Custom units: "-1d", "-1w", "-1M", "-1y" + // Supported: ns, us/µs, ms, s, m, h, d (days), w (weeks), + // M (months=30.44d), y (years=365.25d). + // Use negative values to specify time in the past. + string duration = 2; + } + + // Batch size for deletion (default 10000, max 50000). + // Controls how many events are deleted per database transaction to avoid + // holding large locks. + uint32 batch_size = 3; +} + +message DeleteForwardingHistoryResponse { + // Number of forwarding events deleted. + uint64 events_deleted = 1; + + // Total fees earned from deleted events (in millisatoshis). + // This is the sum of (amt_in - amt_out) for all deleted events, which + // can be used for accounting purposes. + int64 total_fee_msat = 2; + + // Status message. + string status = 3; } \ No newline at end of file diff --git a/lnrpc/routerrpc/router.swagger.json b/lnrpc/routerrpc/router.swagger.json index 996ead61639..2d95c8f850c 100644 --- a/lnrpc/routerrpc/router.swagger.json +++ b/lnrpc/routerrpc/router.swagger.json @@ -1404,6 +1404,25 @@ } } }, + "routerrpcDeleteForwardingHistoryResponse": { + "type": "object", + "properties": { + "events_deleted": { + "type": "string", + "format": "uint64", + "description": "Number of forwarding events deleted." + }, + "total_fee_msat": { + "type": "string", + "format": "int64", + "description": "Total fees earned from deleted events (in millisatoshis).\nThis is the sum of (amt_in - amt_out) for all deleted events, which\ncan be used for accounting purposes." + }, + "status": { + "type": "string", + "description": "Status message." + } + } + }, "routerrpcFailureDetail": { "type": "string", "enum": [ diff --git a/lnrpc/routerrpc/router_backend.go b/lnrpc/routerrpc/router_backend.go index 377d0be3ea0..ebcee8bb3e3 100644 --- a/lnrpc/routerrpc/router_backend.go +++ b/lnrpc/routerrpc/router_backend.go @@ -14,6 +14,7 @@ import ( "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/wire" sphinx "github.com/lightningnetwork/lightning-onion" + "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/clock" "github.com/lightningnetwork/lnd/feature" "github.com/lightningnetwork/lnd/fn/v2" @@ -123,6 +124,23 @@ type RouterBackend struct { // Clock is the clock used to validate payment requests expiry. // It is useful for testing. Clock clock.Clock + + // ForwardingLog provides access to forwarding log database operations. + ForwardingLog ForwardingLogDB +} + +// ForwardingLogDB defines the interface for forwarding log database operations. +// This interface allows the router RPC to interact with the forwarding log +// without depending directly on the channeldb implementation, making testing +// and future refactoring easier. +type ForwardingLogDB interface { + // DeleteForwardingEvents deletes all forwarding events older than the + // specified endTime. The deletion is performed in batches of the given + // size to avoid holding large database locks. It returns statistics + // about the deletion including the number of events deleted and the + // total fees earned from those events. + DeleteForwardingEvents(endTime time.Time, batchSize int) ( + channeldb.DeleteStats, error) } // MissionControl defines the mission control dependencies of routerrpc. diff --git a/lnrpc/routerrpc/router_grpc.pb.go b/lnrpc/routerrpc/router_grpc.pb.go index 6e7e980fc84..d6c60043d8c 100644 --- a/lnrpc/routerrpc/router_grpc.pb.go +++ b/lnrpc/routerrpc/router_grpc.pb.go @@ -131,6 +131,15 @@ type RouterClient interface { // XFindBaseLocalChanAlias is an experimental API that looks up the base scid // for a local chan alias that was registered during the current runtime. XFindBaseLocalChanAlias(ctx context.Context, in *FindBaseAliasRequest, opts ...grpc.CallOption) (*FindBaseAliasResponse, error) + // lncli: `deletefwdhistory` + // DeleteForwardingHistory allows the caller to delete forwarding history + // events older than a specified time. This is useful for implementing data + // retention policies for privacy purposes. The call deletes events in batches + // and returns statistics including the total number of events deleted and the + // aggregate fees earned from those events. The deletion is performed in a + // transaction-safe manner with configurable batch sizes to avoid holding + // large database locks. + DeleteForwardingHistory(ctx context.Context, in *DeleteForwardingHistoryRequest, opts ...grpc.CallOption) (*DeleteForwardingHistoryResponse, error) } type routerClient struct { @@ -493,6 +502,15 @@ func (c *routerClient) XFindBaseLocalChanAlias(ctx context.Context, in *FindBase return out, nil } +func (c *routerClient) DeleteForwardingHistory(ctx context.Context, in *DeleteForwardingHistoryRequest, opts ...grpc.CallOption) (*DeleteForwardingHistoryResponse, error) { + out := new(DeleteForwardingHistoryResponse) + err := c.cc.Invoke(ctx, "/routerrpc.Router/DeleteForwardingHistory", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // RouterServer is the server API for Router service. // All implementations must embed UnimplementedRouterServer // for forward compatibility @@ -609,6 +627,15 @@ type RouterServer interface { // XFindBaseLocalChanAlias is an experimental API that looks up the base scid // for a local chan alias that was registered during the current runtime. XFindBaseLocalChanAlias(context.Context, *FindBaseAliasRequest) (*FindBaseAliasResponse, error) + // lncli: `deletefwdhistory` + // DeleteForwardingHistory allows the caller to delete forwarding history + // events older than a specified time. This is useful for implementing data + // retention policies for privacy purposes. The call deletes events in batches + // and returns statistics including the total number of events deleted and the + // aggregate fees earned from those events. The deletion is performed in a + // transaction-safe manner with configurable batch sizes to avoid holding + // large database locks. + DeleteForwardingHistory(context.Context, *DeleteForwardingHistoryRequest) (*DeleteForwardingHistoryResponse, error) mustEmbedUnimplementedRouterServer() } @@ -679,6 +706,9 @@ func (UnimplementedRouterServer) XDeleteLocalChanAliases(context.Context, *Delet func (UnimplementedRouterServer) XFindBaseLocalChanAlias(context.Context, *FindBaseAliasRequest) (*FindBaseAliasResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method XFindBaseLocalChanAlias not implemented") } +func (UnimplementedRouterServer) DeleteForwardingHistory(context.Context, *DeleteForwardingHistoryRequest) (*DeleteForwardingHistoryResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteForwardingHistory not implemented") +} func (UnimplementedRouterServer) mustEmbedUnimplementedRouterServer() {} // UnsafeRouterServer may be embedded to opt out of forward compatibility for this service. @@ -1096,6 +1126,24 @@ func _Router_XFindBaseLocalChanAlias_Handler(srv interface{}, ctx context.Contex return interceptor(ctx, in, info, handler) } +func _Router_DeleteForwardingHistory_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteForwardingHistoryRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RouterServer).DeleteForwardingHistory(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/routerrpc.Router/DeleteForwardingHistory", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RouterServer).DeleteForwardingHistory(ctx, req.(*DeleteForwardingHistoryRequest)) + } + return interceptor(ctx, in, info, handler) +} + // Router_ServiceDesc is the grpc.ServiceDesc for Router service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -1159,6 +1207,10 @@ var Router_ServiceDesc = grpc.ServiceDesc{ MethodName: "XFindBaseLocalChanAlias", Handler: _Router_XFindBaseLocalChanAlias_Handler, }, + { + MethodName: "DeleteForwardingHistory", + Handler: _Router_DeleteForwardingHistory_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/lnrpc/routerrpc/router_server.go b/lnrpc/routerrpc/router_server.go index 1dbc19e47f9..973992c11a2 100644 --- a/lnrpc/routerrpc/router_server.go +++ b/lnrpc/routerrpc/router_server.go @@ -163,6 +163,10 @@ var ( Entity: "offchain", Action: "write", }}, + "/routerrpc.Router/DeleteForwardingHistory": {{ + Entity: "offchain", + Action: "write", + }}, } // DefaultRouterMacFilename is the default name of the router macaroon @@ -1820,3 +1824,175 @@ func (s *Server) UpdateChanStatus(_ context.Context, } return &UpdateChanStatusResponse{}, nil } + +// DeleteForwardingHistory deletes forwarding history events older than a +// specified time. This method is useful for implementing data retention +// policies for privacy purposes. +func (s *Server) DeleteForwardingHistory(ctx context.Context, + req *DeleteForwardingHistoryRequest) (*DeleteForwardingHistoryResponse, + error) { + + // First, determine the delete-before time based on the request. + var deleteBeforeTime time.Time + switch timeSpec := req.TimeSpec.(type) { + case *DeleteForwardingHistoryRequest_DeleteBeforeTime: + deleteBeforeTime = time.Unix( + int64(timeSpec.DeleteBeforeTime), 0, + ) + + case *DeleteForwardingHistoryRequest_Duration: + // Parse duration using hybrid approach: try standard library + // first, fall back to custom units (d, w, M, y) if needed. + duration, err := parseDuration(timeSpec.Duration) + if err != nil { + return nil, fmt.Errorf("invalid duration format: %w", err) + } + + // Calculate the absolute time by adding the duration (which + // should be negative) to now. + deleteBeforeTime = time.Now().Add(duration) + + default: + return nil, fmt.Errorf("time specification required: either " + + "delete_before_time or duration must be provided") + } + + // Security validation: prevent accidental deletion of very recent data. + // Require that the delete time is at least 1 second in the past. This + // provides a minimal safety check while allowing integration tests to + // work. In production, users typically delete much older data (days/ + // weeks/months old). + // + // TODO(roasbeef): Consider making this configurable or using a longer + // duration (e.g., 1 hour) for production with a way to override for + // testing. + minimumAge := 1 * time.Second + if time.Since(deleteBeforeTime) < minimumAge { + return nil, fmt.Errorf("delete_before_time must be at "+ + "least %v "+ + "in the past to prevent accidental deletion of recent "+ + "data (requested time: %v, current time: %v)", + minimumAge, deleteBeforeTime, time.Now()) + } + + // Set default batch size if not specified. + batchSize := int(req.BatchSize) + if batchSize == 0 { + batchSize = 10000 + } + + log.Infof("DeleteForwardingHistory: deleting events before %v with "+ + "batch size %d", deleteBeforeTime, batchSize) + + // Call the database deletion method. + stats, err := s.cfg.RouterBackend.ForwardingLog.DeleteForwardingEvents( + deleteBeforeTime, batchSize, + ) + if err != nil { + return nil, fmt.Errorf("failed to delete forwarding events: %w", + err) + } + + log.Infof("DeleteForwardingHistory: deleted %d events, total fees: "+ + "%d msat", stats.NumEventsDeleted, stats.TotalFeeMsat) + + return &DeleteForwardingHistoryResponse{ + EventsDeleted: stats.NumEventsDeleted, + TotalFeeMsat: stats.TotalFeeMsat, + Status: fmt.Sprintf("Successfully deleted %d forwarding events", + stats.NumEventsDeleted), + }, nil +} + +// parseDuration parses a duration string using a hybrid approach. It first +// attempts to use the standard library time.ParseDuration, which supports +// ns, us, ms, s, m, h. If that fails, it falls back to custom parsing for +// user-friendly units: d (days), w (weeks), M (months), y (years). +// +// Examples: +// - Standard Go: "-24h", "-1.5h", "-30m" +// - Custom units: "-1d", "-1w", "-1M", "-1y" +// +// All durations should be negative to indicate "time ago". +func parseDuration(durationStr string) (time.Duration, error) { + // First, try the standard library parser. + duration, err := time.ParseDuration(durationStr) + if err == nil { + // Enforce negative durations to prevent confusion. + if duration >= 0 { + return 0, fmt.Errorf("duration must be negative to " + + "indicate time in the past (e.g., -1w, -24h)") + } + return duration, nil + } + + // Fall back to custom parsing for d, w, M, y units. + if len(durationStr) < 2 { + return 0, fmt.Errorf("duration too short") + } + + // Duration strings should start with a minus sign for "ago". + if durationStr[0] != '-' { + return 0, fmt.Errorf("duration must be " + + "negative (e.g., -1w, -24h)") + } + + // Strip the minus sign. + durationStr = durationStr[1:] + + // Find where the numeric part ends. + var ( + numStr string + unit string + ) + for i, ch := range durationStr { + if ch < '0' || ch > '9' { + numStr = durationStr[:i] + unit = durationStr[i:] + break + } + } + + if numStr == "" { + return 0, fmt.Errorf("no numeric value found") + } + if unit == "" { + return 0, fmt.Errorf("no unit specified") + } + + var value int + _, parseErr := fmt.Sscanf(numStr, "%d", &value) + if parseErr != nil { + return 0, fmt.Errorf("invalid numeric value: %w", parseErr) + } + + // Calculate the duration based on the custom unit. + var customDuration time.Duration + switch unit { + case "d": + customDuration = time.Duration(value) * 24 * time.Hour + + case "w": + customDuration = time.Duration(value) * 7 * 24 * time.Hour + + case "M": + // Average month = 30.44 days. + customDuration = time.Duration( + float64(value) * 30.44 * 24 * float64(time.Hour), + ) + + case "y": + // Average year = 365.25 days. + customDuration = time.Duration( + float64(value) * 365.25 * 24 * float64(time.Hour), + ) + + default: + // Not a custom unit we recognize, return the original error. + return 0, fmt.Errorf("unknown time unit: %s (supported: ns, "+ + "us, ms, s, m, h, d, w, M, y)", unit) + } + + // Return negative duration (going back in time). + return -customDuration, nil +} diff --git a/lntest/rpc/router.go b/lntest/rpc/router.go index ccc7b0ef62a..d368e660483 100644 --- a/lntest/rpc/router.go +++ b/lntest/rpc/router.go @@ -283,3 +283,19 @@ func (h *HarnessRPC) TrackPaymentV2(payHash []byte) TrackPaymentClient { return client } + +// DeleteForwardingHistory makes a RPC call to the node's RouterClient and +// asserts. +// +//nolint:ll +func (h *HarnessRPC) DeleteForwardingHistory( + req *routerrpc.DeleteForwardingHistoryRequest) *routerrpc.DeleteForwardingHistoryResponse { + + ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout) + defer cancel() + + resp, err := h.Router.DeleteForwardingHistory(ctxt, req) + h.NoError(err, "DeleteForwardingHistory") + + return resp +} diff --git a/rpcserver.go b/rpcserver.go index d3d3c518014..eeb8151893f 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -773,6 +773,7 @@ func (r *rpcServer) addDeps(ctx context.Context, s *server, EndorsementExperimentEnd, ) }, + ForwardingLog: s.miscDB.ForwardingLog(), } genInvoiceFeatures := func() *lnwire.FeatureVector {