Skip to content

LSPS2: Add TimeProvider trait for expiry checks in no-std and std builds #3746

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

martinsaposnic
Copy link

Closes #3471

As discussed here #3662 (comment)

This PR introduces a TimeProvider trait to enable expiry checks for OpeningFeeParams on LSPS2 in both std and no-std environments. Previously, expiry validation was skipped in no-std builds due to the lack of a system clock.

With this change:

  • A TimeProvider trait is defined, allowing users to supply their own time source.
  • A DefaultTimeProvider implementation is provided for std environments.
  • All expiry checks (is_past, is_expired_opening_fee_params, etc.) now require a TimeProvider instance.
  • The LSPS2 service, utility functions, and tests are updated to use the new trait.
  • The API is extended to allow injecting a custom time provider, ensuring expiry checks are always performed, even in no-std builds.

This abstraction will be even more useful for LSPS5, which relies on time-based logic much more extensively than LSPS2.

- Add 'time' feature flag to allow disabling time-dependent functionality.
- Include 'time' in default features
- It will allow users to disable SystemTime::now without disabling all std features
- Provides a single method duration_since_epoch() -> Duration
- Also a DefaultTimeProvider implementation is provided that uses std::time
@ldk-reviews-bot
Copy link

ldk-reviews-bot commented Apr 20, 2025

👋 Thanks for assigning @tnull as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

- is_past() on LSPSDateTime is changed so it receives a time_provider to get current time.
- is_expired_opening_fee_params() now receives a time_provider to pass to is_past()
- is_expired_opening_fee_params now does not depend on std being enabled.
- LSPS2 service now passes around the time_provider to be able to use wherever necessary.
- LSPS2 service now provides two new() methods:
1. a default new() that uses DefaultTimeProvider
2. a new_with_custom_time_provider() that expects a custom time_provider
Devices with no access to std will need to use the latter
Copy link

codecov bot commented Apr 20, 2025

Codecov Report

Attention: Patch coverage is 77.77778% with 16 lines in your changes missing coverage. Please review.

Project coverage is 89.08%. Comparing base (c6921fa) to head (0f17d59).

Files with missing lines Patch % Lines
lightning-liquidity/src/lsps2/service.rs 70.96% 9 Missing ⚠️
lightning-liquidity/src/manager.rs 53.33% 7 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3746      +/-   ##
==========================================
- Coverage   89.15%   89.08%   -0.08%     
==========================================
  Files         156      156              
  Lines      123837   123875      +38     
  Branches   123837   123875      +38     
==========================================
- Hits       110408   110348      -60     
- Misses      10754    10835      +81     
- Partials     2675     2692      +17     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Contributor

@joostjager joostjager left a comment

Choose a reason for hiding this comment

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

I probably still miss some context, but kept thinking why not just inject the dependency always and let the user instantiate either the std time provider or their own.

/// This trait is used to provide the current time service operations and to convert between timestamps and durations.
pub trait TimeProvider {
/// Get the current time as a duration since the Unix epoch.
fn duration_since_epoch(&self) -> Duration;
Copy link
Contributor

Choose a reason for hiding this comment

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

I know this name is sort of standard in LDK, but to me something like unix_time would have been better. Duration is already the type, and epoch doesn't specify which epoch.

Copy link
Contributor

Choose a reason for hiding this comment

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

I disagree, exactly because you'd expect unix_time to be a SystemTime or u64, but not a Duration. Plus, homogeneity is an important API feature, so keeping this in-line with the rest of internal APIs is a big plus, IMO.

Copy link
Contributor

Choose a reason for hiding this comment

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

No need to change it now. As you say, keeping it consistent is a good API feature. For me as a developer though, this isn't the most descriptive name.

#[cfg(feature = "time")]
{
// Use default new if time feature is enabled and no custom provider
LSPS2ServiceHandler::new(
Copy link
Contributor

Choose a reason for hiding this comment

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

Can't TimeProvider be simply required here (no option), and then instantiate or panic higher up in the call stack? Also no need for two new variants then.


/// Default time provider using the system clock.
#[derive(Clone, Debug)]
#[cfg(feature = "time")]
Copy link
Contributor

Choose a reason for hiding this comment

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

I re-read the PR description, but was still thinking why isn't this just std gated?

Copy link
Author

Choose a reason for hiding this comment

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

Sorry for not mentioning this in the PR description (I’ll update it shortly). This change comes from a discussion in this PR, specifically this comment from tnull:

Actually, now that we went this way, let's not feature gate this behind std, but rather a new time feature. Context is the following: on WASM, you generally have access to std, but not entirely. For example, time-dependent behaviors like SystemTime::now would just panic at runtime. However, when previously feature-gating on std, we good feedback from some WASM users that would like to keep using the non-time std features.
TLDR: would be nice to allow users to disable the SystemTime::now default without disabling the entirety of std. So let's introduce a time feature, as we did in other crates (cf. lightning-transaction-sync).

@tnull tagging Elias for review and input.

Copy link
Contributor

@joostjager joostjager Apr 21, 2025

Choose a reason for hiding this comment

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

But if those users run with std and just don't inject the panic'ing default std time provider and instead supply their own, aren't they good with the time flag?

Copy link
Contributor

Choose a reason for hiding this comment

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

But if those users run with std and just don't inject the panic'ing default std time provider and instead supply their own, aren't they good with the time flag?

Yes, but we still shouldn't expose a 'default' API that would panic if used? That said, given that it requires std, we should probably feature-gate this on all(feature = "std", feature = "time"), or have time enable std.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, but we still shouldn't expose a 'default' API that would panic if used?

If some platforms have an incomplete implementation of the std feature, is it the responsibility of this library to protect against hitting the missing part via a feature flag?

I also wouldn't make it a 'default' API, but just require a time provider to be supplied. Overall it seems to me that a time flag may be unnecessary.

@@ -14,8 +14,9 @@ categories = ["cryptography::cryptocurrencies"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[features]
default = ["std"]
default = ["std", "time"]
Copy link
Contributor

Choose a reason for hiding this comment

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

Disabling SystemTIme::now without disabling all of std: couldn't this be realized by just passing in another time provider than the std one, without requiring this flag?

@ldk-reviews-bot
Copy link

👋 The first review has been submitted!

Do you think this PR is ready for a second reviewer? If so, click here to assign a second reviewer.

@tnull tnull self-requested a review April 21, 2025 16:59
Copy link
Contributor

@tnull tnull left a comment

Choose a reason for hiding this comment

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

Thanks for looking into this, though I'm not convinced moving forward with this (and reviewing it in parallel) makes sense until #3662 lands. At least if you don't want to rebase this PR on the entirety of #3662, which could get very unwieldy given the number of commit over there.

/// This trait is used to provide the current time service operations and to convert between timestamps and durations.
pub trait TimeProvider {
/// Get the current time as a duration since the Unix epoch.
fn duration_since_epoch(&self) -> Duration;
Copy link
Contributor

Choose a reason for hiding this comment

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

I disagree, exactly because you'd expect unix_time to be a SystemTime or u64, but not a Duration. Plus, homogeneity is an important API feature, so keeping this in-line with the rest of internal APIs is a big plus, IMO.


/// Default time provider using the system clock.
#[derive(Clone, Debug)]
#[cfg(feature = "time")]
Copy link
Contributor

Choose a reason for hiding this comment

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

But if those users run with std and just don't inject the panic'ing default std time provider and instead supply their own, aren't they good with the time flag?

Yes, but we still shouldn't expose a 'default' API that would panic if used? That said, given that it requires std, we should probably feature-gate this on all(feature = "std", feature = "time"), or have time enable std.

/// Trait defining a time provider
///
/// This trait is used to provide the current time service operations and to convert between timestamps and durations.
pub trait TimeProvider {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm a bit confused why this is re-added here, as we're already adding it (in a different place) in #3662. Maybe it would be good to put this PR in draft until #3662 has been merged, or rebase it on top of #3662, as changes there will need to be consequentially accounted for here?

Copy link
Author

Choose a reason for hiding this comment

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

This PR is extracting some of the code from LSPS5 PR. My idea was to get this PR merged and then rebase LSPS5 on top of it, that way we would lose some commits from the LSPS5 PR and make it a bit smaller (same idea with the solution to this issue #3459 (PR coming soon), where I'm extracting some of the code written in the LSPS5 PR and putting it in a new PR, that way making the LSPS5 PR smaller)

@TheBlueMatt
Copy link
Collaborator

I do kinda wonder if this is worth it? What is the practical user impact of not checking for expiry? Presumably the LSP will simply fail the attempt to use the past offers and the client will simply request new offers, assuming it has past offers, but currently we don't persist offers (and probably shouldn't?) so in practice we'll basically always be requesting fresh offers anyway.

@tnull
Copy link
Contributor

tnull commented Apr 29, 2025

I do kinda wonder if this is worth it? What is the practical user impact of not checking for expiry? Presumably the LSP will simply fail the attempt to use the past offers and the client will simply request new offers, assuming it has past offers, but currently we don't persist offers (and probably shouldn't?) so in practice we'll basically always be requesting fresh offers anyway.

Note that the TimeProvider trait is already introduced by the LSPS5 work in #3662 (where timestamps are part of the message signing) and will here just be reused to fix the missing check in the LSPS2 service.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

LSPS2: Check OpeningFeeParams expiry times in no-std
5 participants