Skip to content

Conversation

@Mshehu5
Copy link
Contributor

@Mshehu5 Mshehu5 commented Aug 29, 2025

Fixes #893

Problem

When roundtrip testing a new concrete v2::PjParams struct, SystemTime's nanosecond precision causes test failures. After serializing SystemTime into the EX1 u32 expiration parameter and deserializing back, precision is lost and assert_eq fails. This highlights a fundamental mismatch between SystemTime's nanosecond precision and the protocol's second-precision requirements.

Additionally, SystemTime is not available in WASM environments, limiting the crate's portability.

Solution

Replace std::time::SystemTime with bitcoin::absolute::Time throughout the v2 payjoin implementation for the following reasons:

Why bitcoin::absolute::Time over alternatives:

bitcoin::absolute::Time (Chosen)

  • Wraps a u32 Unix timestamp, matching payjoin's precision exactly
  • Maintained by rust-bitcoin team with extensive testing
  • Built-in error handling and conversion methods
  • WASM-compatible and already in dependency tree
  • Clear semantic meaning and type safety
  • Eliminates roundtrip precision loss

Changes

  • Core refactoring: Replace all SystemTime usage with bitcoin::absolute::Time
  • Helper functions: Add now() and now_as_unix_seconds() utility functions in uri::v2 module
  • Type updates: Convert expiration fields in SessionContext and error types to use Time
  • Precision fix: Ensure roundtrip serialization/deserialization maintains exact values
  • WASM compatibility: Enable usage in WASM environments where SystemTime is unavailable

This change eliminates potential conversion issues between different time representations and provides better integration with Bitcoin's timestamp handling patterns used elsewhere in the codebase."

##AI disclosure
i used clause to understand the problem better i also used it to cross check the system::Time implementations in code base i also consulted Claude if the direction is okay

Please confirm the following before requesting review:

Convert timestamp handling throughout the v2 implementation to use
bitcoin::absolute::Time instead of std::time::SystemTime for better
consistency with Bitcoin ecosystem conventions.

Add helper functions now() and now_as_unix_seconds() in uri::v2 module
to centralize time operations. Update SessionContext, error types, and
all related functionality to work with the new Time type.

This change eliminates potential conversion issues between different
time representations and provides better integration with Bitcoin's
timestamp handling patterns used elsewhere in the codebase."
Copy link
Collaborator

@nothingmuch nothingmuch left a comment

Choose a reason for hiding this comment

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

I didn't review very thoroughly, since I have some feedback on the approach.

I would prefer if this wrapped bitcoin::absolute::Time in our own struct, so that the bitcoin Time isn't part of our public API but just an implementation detail, and so that we can support typesafe time calculations

Comment on lines +283 to +284
let now_seconds = crate::uri::v2::now_as_unix_seconds();
let expiry_seconds = now_seconds + TWENTY_FOUR_HOURS_DEFAULT_EXPIRY.as_secs() as u32;
Copy link
Collaborator

@nothingmuch nothingmuch Aug 29, 2025

Choose a reason for hiding this comment

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

Since part of the goal of this change is using the typesystem to represent the time values we support more accurately, I think would prefer if instead of a allowing raw u32s for this, you defined a newtype Time(bitcoin::absolute::Time) which supports addition with our own Duration(u32) type, which has TryFrom<std::time::Duration> that forbids subsecond resolution.

Copy link
Contributor

Choose a reason for hiding this comment

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

agree


/// Get the current time as a bitcoin::absolute::Time with second precision.
pub(crate) fn now() -> Time {
Time::from_consensus(now_as_unix_seconds())
Copy link
Collaborator

Choose a reason for hiding this comment

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

with a newtype of our own, we this could be SystemTime::now().into() given a From<SystemTime> impl

Ok(Self(session_context))
}

pub fn with_expiry(self, expiry: Duration) -> Self {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is not a suggestion, at least not yet, but perhaps this could take an expiry: IntoTime where IntoTime is a sealed trait like IntoUri, and then Duration could convert to Time by being added to now(). Having such a conversion seems unwise for general time handling but i think since the only purpose is specifying timeouts in our API that should be fine. Then the conversion and use of the clock can be feature gated for nostd builds without requiring two different APIs, in std builds you can pass a duration or an absolute time, and in e.g. wasm builds you have to provide an abs time.

@DanGould what do you think?

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd rather get this across the line with the simplest type (Time?) in the API instead of Duration and then am open to a follow up with a sealed trait that lets you pass Duration as IntoTime for std builds.

Copy link
Collaborator

Choose a reason for hiding this comment

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

i think that would be fine from a semver PoV as strictly speaking it would only expand the set of types that are allowed to appear in that argument position

@DanGould
Copy link
Contributor

I would prefer if this wrapped bitcoin::absolute::Time in our own struct, so that the bitcoin Time isn't part of our public API but just an implementation detail

I could go either way on this. We've already got rust-bitcoin types all over the place in our public API with no plans to remove it. Can we not support typesafe time calculations without this?

@nothingmuch
Copy link
Collaborator

nothingmuch commented Aug 31, 2025

I could go either way on this. We've already got rust-bitcoin types all over the place in our public API with no plans to remove it. Can we not support typesafe time calculations without this?

we could but only with an extension trait, and arguably it's not typesafe in the same sense, you could inadvertantly take a transaction's nlocktime value parsed into a Seconds(Time) and then pass the Time value in as an expiry time, even though that probably doesn't make sense. this might happen accidentally due to variable shadowing or something like that, e.g. let tiem = what_i_meant() but time var of the same type was defined in an outer scope (though this hypothetical example would generate a warning since tiem will be unused).

using the newtype pattern means we specify a BIP 77 expiry time that only happens to overlap in definition with the seconds based bitcoin consensus version, but they are statically distinguishable from each other. additionally since we introduce it, we can impl whatever we want for it, whereas for the bitcoin units' Time struct we can only define an extension trait, that seems more complex not simpler to me

@arminsabouri
Copy link
Collaborator

Replaced by #1047

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.

std::time::SystemTime has nanosecond precision but Payjoin only sees u32 UnixTime second precision

4 participants