Skip to content

Conversation

@Roasbeef
Copy link
Member

In this PR, we add a new RPC call DeleteForwardingEvents. The new calls allows users to specify a timestamp, or a duration (-1d, -1y, etc) to be used to delete all forwarding events before said timestamp. After deletion, the call also returns the total amount of fees earned in that period.

Some additional documentation, is added to explain how the call works, and best practices w.r.t using it.

Fixes #9963

In this commit, we add a new method to delete old forwarding events
from the database, addressing issue lightningnetwork#9963 where LSPs need a way to
implement data retention policies without node migration.

The implementation uses batched deletion to avoid holding large
database transactions that could block other operations. Events are
deleted in configurable batches (default 10k, maximum 50k) with
cursor-based iteration for memory efficiency. Each batch runs in its
own transaction, allowing concurrent operations to proceed between
batches.

The method calculates and returns total fees (sum of AmtIn - AmtOut)
from deleted events, allowing operators to maintain aggregate
financial records for tax reporting even after deleting detailed
surveillance data. This separation of financial accounting from
detailed event logs is essential for privacy-conscious operators.

Security validations include a minimum age requirement (currently 1
second for testing, intended to be configurable) to prevent
accidental deletion of recent data, and batch size limits to prevent
resource exhaustion attacks.
In this commit, we add extensive test coverage for the forwarding
event deletion functionality, achieving over 85% coverage as
required. The test suite includes eight distinct test cases covering
all critical paths and edge cases.

The basic tests verify correct deletion counts, fee calculations, and
time boundary handling. We test partial deletion (ensuring only
events before a cutoff are deleted), batch processing (verifying
multiple batches work correctly), and idempotency (confirming
repeated deletions are safe).

The most interesting addition is property-based testing using the
rapid package. This test generates 100 randomized scenarios and
validates five key invariants: deleted count accuracy, fee
calculation correctness, query result consistency, timestamp boundary
enforcement, and idempotent behavior. This approach catches edge
cases that traditional example-based tests might miss, particularly
around boundary conditions and concurrent access patterns.

We also test degenerate cases like empty databases and exact time
boundaries to ensure robust error handling throughout.
In this commit, we define the new DeleteForwardingHistory RPC in the
Router sub-server protocol, providing a clean API for privacy-focused
deletion of old forwarding events.

The RPC uses a oneof for time specification, allowing callers to
provide either an absolute Unix timestamp (delete_before_time) or a
relative duration string (duration like "-1w" or "-1M"). This
flexibility accommodates both precise compliance deadlines and
convenient regular cleanup schedules. The duration format supports
seconds, minutes, hours, days, weeks, months, and years.

The response includes the count of deleted events and total fees in
millisatoshis, enabling operators to maintain financial records while
discarding detailed routing surveillance data. The status field
provides human-readable feedback about the operation.
In this commit, we regenerate the protocol buffer code using make rpc
to produce the Go client/server interfaces, JSON bindings, and
Swagger documentation for the new DeleteForwardingHistory RPC.
In this commit, we implement the server-side handler for the
DeleteForwardingHistory RPC, connecting the protocol definition to
the database layer through our ForwardingLogDB interface.

The handler supports two time specification formats via the oneof
field. For absolute timestamps, we convert directly from Unix seconds
to a time.Time. For relative duration strings, we implement a
parseDuration helper that supports natural time units (s, m, h, d, w,
M for months, y for years) with negative values indicating "time ago"
semantics. This makes invocations like "-1M" (one month ago) work
intuitively at the CLI.

Security is a key consideration in this implementation. We validate
that the delete_before_time is at least 1 second in the past to
prevent accidental deletion of very recent data. The current 1-second
threshold is intentionally minimal for integration testing; in
production deployments, operators would typically configure a longer
minimum age (like 1 hour or 1 day) to provide additional safety
margins. We also enforce batch size limits and provide comprehensive
logging for audit trails.

The response includes aggregated fee totals to support operators who
need to maintain financial records separate from detailed routing
surveillance data. This aligns with privacy-preserving accounting
practices where aggregate revenue can be tracked without retaining
granular event history.
In this commit, we extend the test harness RPC wrapper to include the
new DeleteForwardingHistory method, following the established pattern
for router RPC calls. This enables integration tests to invoke the
deletion functionality with automatic error handling and logging.
In this commit, we connect the forwarding log database interface to
the router RPC backend, completing the dependency injection chain
from the RPC handler down to the database layer. This single-line
change enables the DeleteForwardingHistory RPC to access the actual
forwarding event database through the clean ForwardingLogDB
interface abstraction.
In this commit, we add a new lncli command for deleting forwarding
history, providing a user-friendly interface to the underlying RPC.
The command follows the pattern established by similar commands like
deletepayments.

Users can specify deletion criteria using either --duration for
relative time ("--duration=-1M" for events older than one month) or
--before for absolute Unix timestamps. The --batch_size flag allows
tuning the deletion batch size for performance, though the default of
10k events per batch should work well for most deployments.

The implementation includes an interactive confirmation prompt that
displays exactly what will be deleted and requests explicit user
confirmation. This safety measure helps prevent accidental deletion
of important data. The prompt can be bypassed in scripted
environments by piping "yes" to the command.

After successful deletion, the command displays comprehensive
statistics including the number of events deleted and total fees
earned during that period. This fee information is critical for
operators who need to maintain financial records for tax reporting
even after purging detailed routing surveillance data for privacy.
In this commit, we add end-to-end integration tests that exercise the
complete forwarding history deletion flow through multi-node Lightning
Network topologies. The tests verify the feature works correctly in
realistic scenarios with actual payment routing and database
operations.

We implement five distinct test scenarios. The basic deletion test
creates a three-node network (Alice → Bob → Carol), routes multiple
payments through Bob to generate forwarding events, then verifies that
deletion works correctly with accurate event counts and fee
calculations. The partial deletion test validates that only events
before a specific cutoff time are deleted, leaving newer events
intact. The empty database test ensures graceful handling when no
events exist. The idempotency test confirms that repeated deletions
with the same parameters safely do nothing after the first deletion.
Finally, the time formats test validates both absolute timestamp and
relative duration specifications work correctly.

A critical implementation detail is timing. The minimum age validation
requires events to be at least 1 second old before deletion. To
satisfy this in tests without excessive delays, we sleep for 1 second
after each payment, then use a timestamp 2 seconds in the past for
deletion. This ensures events are old enough while keeping total test
time under 125 seconds for all five scenarios.
In this commit, we add detailed documentation explaining the privacy
implications of forwarding history retention and how to use the new
deletion feature effectively. The document is written in natural
prose with technical depth appropriate for node operators and LSPs.

The guide begins by explaining why this feature matters. Lightning
routing nodes accumulate detailed surveillance data about payment
flows over time. While this data is useful for debugging and
analytics, it poses significant privacy risks if the node is
compromised or subpoenaed. For LSPs handling customer payments,
retention of detailed forwarding logs may conflict with data
protection regulations like GDPR. The deletion feature allows
operators to implement sensible retention policies without the
extreme measure of migrating to a new node instance.

We include two mermaid diagrams to visualize the system. The sequence
diagram shows the complete RPC flow including batch processing and fee
calculation. The graph diagram illustrates how fees are accumulated
from individual events during deletion to provide aggregate financial
records.

The usage section provides concrete examples for common scenarios:
deleting old data on a regular schedule, one-time historical purges,
and automated cron-based cleanup. We explain both time specification
formats (relative durations and absolute timestamps) with examples of
when each is appropriate.

The document also covers automation strategies, troubleshooting
common issues, performance characteristics of batch deletion, and
legal/compliance considerations. We emphasize best practices like
maintaining separate financial records, testing deletion policies in
staging environments, and understanding that deletion is permanent
and irreversible.

This documentation ensures operators can use the feature safely and
effectively while understanding the privacy and compliance trade-offs
involved.
@Roasbeef Roasbeef added enhancement Improvements to existing features / behaviour rpc Related to the RPC interface privacy General label for issues/PRs related to the privacy implications of using the software security General label for issues/PRs related to the security of the software labels Oct 24, 2025
@gemini-code-assist
Copy link

Summary of Changes

Hello @Roasbeef, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request implements a crucial privacy-enhancing feature for LND nodes by providing a mechanism to selectively delete old forwarding history. This allows node operators to manage data retention policies effectively, reducing the privacy exposure associated with long-term storage of routing event data, while still providing necessary accounting information for the purged records.

Highlights

  • New RPC for Forwarding History Deletion: Introduced a new RPC call, DeleteForwardingEvents, which allows users to selectively purge old forwarding history from the database based on a specified timestamp or relative duration (e.g., '-1d', '-1y').
  • Accounting for Deleted Events: The deletion operation returns statistics including the total number of events deleted and the aggregate fees earned from those events, facilitating accounting and data retention policies.
  • Batched Deletion for Performance: The deletion process is performed in configurable batches to prevent holding large database locks, ensuring efficient operation even with a large number of events.
  • New lncli Command: A new lncli command, deletefwdhistory, is added to expose this functionality to users, complete with detailed usage instructions and warnings about the irreversible nature of the operation.
  • Comprehensive Documentation and Testing: Extensive documentation has been added to explain the feature's privacy implications, mechanics, security considerations, and best practices. Robust unit and integration tests, including property-based testing, ensure the correctness and idempotency of the deletion logic.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new RPC call, DeleteForwardingEvents, to selectively purge old forwarding history. The implementation is well-structured, including batched database deletions to avoid long-held locks, and it returns useful statistics about the deleted events. The addition of extensive unit, property-based, and integration tests is commendable. I have two main points of feedback: one regarding a safety check's duration and another about clarifying a function's behavior in its comment.

Comment on lines +1869 to +1876
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())
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The minimumAge for deleting forwarding history is set to 1 second. This is a very short duration and could lead to accidental deletion of recent data. The new documentation in docs/forwarding_history_privacy.md states that a one-hour minimum age is enforced. To improve safety and align with the documentation, this value should be increased to one hour. The TODO comment also suggests this was the original intention.

Please note that integration tests in itest/lnd_forward_delete_test.go will need to be adjusted to account for the longer minimum age if this change is made.

Suggested change
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())
}
minimumAge := 1 * time.Hour
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())
}

Comment on lines +431 to +432
// DeleteForwardingEvents deletes all forwarding events older than the specified
// endTime from the database. The deletion is performed in batches to avoid

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The function comment states that it deletes events "older than" endTime. However, the implementation and tests show that it deletes events with a timestamp less than or equal to endTime. To avoid confusion, it would be clearer to state that it deletes events "at or before" the specified endTime.

Suggested change
// DeleteForwardingEvents deletes all forwarding events older than the specified
// endTime from the database. The deletion is performed in batches to avoid
// DeleteForwardingEvents deletes all forwarding events with a timestamp at or
// before the specified endTime from the database. The deletion is performed

@saubyk saubyk added this to v0.21 Oct 24, 2025
@saubyk saubyk moved this to In progress in v0.21 Oct 24, 2025
@saubyk saubyk added this to the v0.21.0 milestone Oct 24, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement Improvements to existing features / behaviour privacy General label for issues/PRs related to the privacy implications of using the software rpc Related to the RPC interface security General label for issues/PRs related to the security of the software

Projects

Status: In progress

Development

Successfully merging this pull request may close these issues.

[feature]: Export a summary of fees earned after a given time, then purge forwarding logs of entries older than that time

2 participants