Skip to content
This repository was archived by the owner on Apr 13, 2021. It is now read-only.

Commit 06bfd37

Browse files
committed
Use std::time::SystemTime instead of u64 for timestamps
1 parent c6b32c0 commit 06bfd37

File tree

5 files changed

+171
-26
lines changed

5 files changed

+171
-26
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
This repo provides datastructures for BOLT 11 lightning invoices.
55
It provides functions to parse and serialize invoices from and to bech32.
66

7+
**Please be sure to run the test suite since we need to check assumptions
8+
regarding `SystemTime`'s bounds on your platform.**
9+
710
## Contributing
811
* same coding style standard as [rust-bitcoin/rust-lightning](https://github.com/rust-bitcoin/rust-lightning)
912
* use tabs and spaces (appropriately)

src/de.rs

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -311,8 +311,7 @@ impl FromBase32 for RawDataPart {
311311
return Err(ParseError::TooShortDataPart);
312312
}
313313

314-
let timestamp: u64 = parse_int_be(&data[0..7], 32)
315-
.expect("7*5bit < 64bit, no overflow possible");
314+
let timestamp = PositiveTimestamp::from_base32(&data[0..7])?;
316315
let tagged = parse_tagged_parts(&data[7..])?;
317316

318317
Ok(RawDataPart {
@@ -322,6 +321,23 @@ impl FromBase32 for RawDataPart {
322321
}
323322
}
324323

324+
impl FromBase32 for PositiveTimestamp {
325+
type Err = ParseError;
326+
327+
fn from_base32(b32: &[u5]) -> Result<Self, Self::Err> {
328+
if b32.len() != 7 {
329+
return Err(ParseError::InvalidSliceLength("PositiveTimestamp::from_base32()".into()));
330+
}
331+
let timestamp: u64 = parse_int_be(b32, 32)
332+
.expect("7*5bit < 64bit, no overflow possible");
333+
match PositiveTimestamp::from_unix_timestamp(timestamp) {
334+
Ok(t) => Ok(t),
335+
Err(CreationError::TimestampOutOfBounds) => Err(ParseError::TimestampOverflow),
336+
Err(_) => unreachable!(),
337+
}
338+
}
339+
}
340+
325341
impl FromBase32 for Signature {
326342
type Err = ParseError;
327343
fn from_base32(signature: &[u5]) -> Result<Self, Self::Err> {
@@ -589,7 +605,8 @@ pub enum ParseError {
589605
InvalidScriptHashLength,
590606
InvalidRecoveryId,
591607
InvalidSliceLength(String),
592-
Skip
608+
Skip,
609+
TimestampOverflow,
593610
}
594611

595612
#[derive(PartialEq, Debug, Clone)]
@@ -648,6 +665,7 @@ impl error::Error for ParseError {
648665
InvalidRecoveryId => "recovery id is out of range (should be in [0,3])",
649666
InvalidSliceLength(_) => "some slice had the wrong length",
650667
Skip => "the tagged field has to be skipped because of an unexpected, but allowed property",
668+
TimestampOverflow => "the invoice's timestamp could not be represented as SystemTime",
651669
}
652670
}
653671
}
@@ -924,7 +942,8 @@ mod test {
924942
fn test_raw_signed_invoice_deserialization() {
925943
use TaggedField::*;
926944
use secp256k1::{RecoveryId, RecoverableSignature, Secp256k1};
927-
use {SignedRawInvoice, Signature, RawInvoice, RawHrp, RawDataPart, Currency, Sha256};
945+
use {SignedRawInvoice, Signature, RawInvoice, RawHrp, RawDataPart, Currency, Sha256,
946+
PositiveTimestamp};
928947

929948
assert_eq!(
930949
"lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmw\
@@ -938,7 +957,7 @@ mod test {
938957
si_prefix: None,
939958
},
940959
data: RawDataPart {
941-
timestamp: 1496314658,
960+
timestamp: PositiveTimestamp::from_unix_timestamp(1496314658).unwrap(),
942961
tagged_fields: vec ! [
943962
PaymentHash(Sha256(Sha256Hash::from_hex(
944963
"0001020304050607080900010203040506070809000102030405060708090102"

src/lib.rs

Lines changed: 134 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,32 @@ use std::ops::Deref;
1313

1414
use std::iter::FilterMap;
1515
use std::slice::Iter;
16+
use std::time::{SystemTime, Duration, UNIX_EPOCH};
1617

1718
mod de;
1819
mod ser;
1920
mod tb;
2021

2122
pub use de::{ParseError, ParseOrSemanticError};
2223

24+
25+
// TODO: fix before 2038 (see rust PR #55527)
26+
/// Defines the maximum UNIX timestamp that can be represented as `SystemTime`. This is checked by
27+
/// one of the unit tests, please run them.
28+
const SYSTEM_TIME_MAX_UNIX_TIMESTAMP: u64 = std::i32::MAX as u64;
29+
30+
/// This function is used as a static assert for the size of `SystemTime`. If the crate fails to
31+
/// compile due to it this indicates that your system uses unexpected bounds for `SystemTime`. You
32+
/// can remove this functions and run the test `test_system_time_bounds_assumptions`. In any case,
33+
/// please open an issue. If all tests pass you should be able to use this library safely by just
34+
/// removing this function till we patch it accordingly.
35+
fn __system_time_size_check() {
36+
/// Use 2 * sizeof(u64) as expected size since the expected underlying implementation is storing
37+
/// a `Duration` since `SystemTime::UNIX_EPOCH`.
38+
unsafe { std::mem::transmute::<SystemTime, [u8; 16]>(UNIX_EPOCH); }
39+
}
40+
41+
2342
/// Builder for `Invoice`s. It's the most convenient and advised way to use this library. It ensures
2443
/// that only a semantically and syntactically correct Invoice can be built using it.
2544
///
@@ -72,7 +91,7 @@ pub struct InvoiceBuilder<D: tb::Bool, H: tb::Bool, T: tb::Bool> {
7291
currency: Currency,
7392
amount: Option<u64>,
7493
si_prefix: Option<SiPrefix>,
75-
timestamp: Option<u64>,
94+
timestamp: Option<PositiveTimestamp>,
7695
tagged_fields: Vec<TaggedField>,
7796
error: Option<CreationError>,
7897

@@ -149,14 +168,22 @@ pub struct RawHrp {
149168
/// Data of the `RawInvoice` that is encoded in the data part
150169
#[derive(Eq, PartialEq, Debug, Clone)]
151170
pub struct RawDataPart {
152-
// TODO: find better fitting type that only allows positive timestamps to avoid checks for negative timestamps when encoding
153-
/// generation time of the invoice as UNIX timestamp
154-
pub timestamp: u64,
171+
/// generation time of the invoice
172+
pub timestamp: PositiveTimestamp,
155173

156174
/// tagged fields of the payment request
157175
pub tagged_fields: Vec<RawTaggedField>,
158176
}
159177

178+
/// A timestamp that refers to a date after 1 January 1970 which means its representation as UNIX
179+
/// timestamp is positive.
180+
///
181+
/// # Invariants
182+
/// The UNIX timestamp representing the stored time has to be positive and small enough so that
183+
/// a `EpiryTime` can be added to it without an overflow.
184+
#[derive(Eq, PartialEq, Debug, Clone)]
185+
pub struct PositiveTimestamp(SystemTime);
186+
160187
/// SI prefixes for the human readable part
161188
#[derive(Eq, PartialEq, Debug, Clone, Copy)]
162189
pub enum SiPrefix {
@@ -442,17 +469,30 @@ impl<D: tb::Bool, T: tb::Bool> InvoiceBuilder<D, tb::False, T> {
442469

443470
impl<D: tb::Bool, H: tb::Bool> InvoiceBuilder<D, H, tb::False> {
444471
/// Sets the timestamp. `time` is a UNIX timestamp.
445-
pub fn timestamp(mut self, time: u64) -> InvoiceBuilder<D, H, tb::True> {
446-
self.timestamp = Some(time);
472+
pub fn timestamp_raw(mut self, time: u64) -> InvoiceBuilder<D, H, tb::True> {
473+
match PositiveTimestamp::from_unix_timestamp(time) {
474+
Ok(t) => self.timestamp = Some(t),
475+
Err(e) => self.error = Some(e),
476+
}
477+
478+
self.set_flags()
479+
}
480+
481+
/// Sets the timestamp.
482+
pub fn timestamp(mut self, time: SystemTime) -> InvoiceBuilder<D, H, tb::True> {
483+
match PositiveTimestamp::from_system_time(time) {
484+
Ok(t) => self.timestamp = Some(t),
485+
Err(e) => self.error = Some(e),
486+
}
487+
447488
self.set_flags()
448489
}
449490

450491
/// Sets the timestamp to the current UNIX timestamp.
451492
pub fn current_timestamp(mut self) -> InvoiceBuilder<D, H, tb::True> {
452493
use std::time::{SystemTime, UNIX_EPOCH};
453-
let now = SystemTime::now();
454-
let since_unix_epoch = now.duration_since(UNIX_EPOCH).expect("it won't be 1970 ever again");
455-
self.timestamp = Some(since_unix_epoch.as_secs() as u64);
494+
let now = PositiveTimestamp::from_system_time(SystemTime::now());
495+
self.timestamp = Some(now.expect("for the foreseeable future this shouldn't happen"));
456496
self.set_flags()
457497
}
458498
}
@@ -717,6 +757,61 @@ impl RawInvoice {
717757
}
718758
}
719759

760+
impl PositiveTimestamp {
761+
762+
/// Create a new `PositiveTimestamp` from a unix timestamp in the Range
763+
/// `0...SYSTEM_TIME_MAX_UNIX_TIMESTAMP / 2`, otherwise return a
764+
/// `CreationError::TimestampOutOfBounds`.
765+
pub fn from_unix_timestamp(unix_seconds: u64) -> Result<Self, CreationError> {
766+
if unix_seconds > SYSTEM_TIME_MAX_UNIX_TIMESTAMP / 2 {
767+
Err(CreationError::TimestampOutOfBounds)
768+
} else {
769+
Ok(PositiveTimestamp(UNIX_EPOCH + Duration::from_secs(unix_seconds)))
770+
}
771+
}
772+
773+
/// Create a new `PositiveTimestamp` from a `SystemTime` with a corresponding unix timestamp in
774+
/// the Range `0...SYSTEM_TIME_MAX_UNIX_TIMESTAMP / 2`, otherwise return a
775+
/// `CreationError::TimestampOutOfBounds`.
776+
pub fn from_system_time(time: SystemTime) -> Result<Self, CreationError> {
777+
if time
778+
.duration_since(UNIX_EPOCH)
779+
.map(|t| t.as_secs() <= SYSTEM_TIME_MAX_UNIX_TIMESTAMP / 2) // check for consistency reasons
780+
.unwrap_or(true)
781+
{
782+
Ok(PositiveTimestamp(time))
783+
} else {
784+
Err(CreationError::TimestampOutOfBounds)
785+
}
786+
}
787+
788+
/// Returns the UNIX timestamp representing the stored time
789+
pub fn as_unix_timestamp(&self) -> u64 {
790+
self.0.duration_since(UNIX_EPOCH)
791+
.expect("ensured by type contract/constructors")
792+
.as_secs()
793+
}
794+
795+
/// Returns a reference to the internal `SystemTime` time representation
796+
pub fn as_time(&self) -> &SystemTime {
797+
&self.0
798+
}
799+
}
800+
801+
impl Into<SystemTime> for PositiveTimestamp {
802+
fn into(self) -> SystemTime {
803+
self.0
804+
}
805+
}
806+
807+
impl Deref for PositiveTimestamp {
808+
type Target = SystemTime;
809+
810+
fn deref(&self) -> &Self::Target {
811+
&self.0
812+
}
813+
}
814+
720815
impl Invoice {
721816
fn into_signed_raw(self) -> SignedRawInvoice {
722817
self.signed_invoice
@@ -788,8 +883,8 @@ impl Invoice {
788883
Ok(invoice)
789884
}
790885

791-
pub fn timestamp(&self) -> u64 {
792-
self.signed_invoice.raw_invoice.data.timestamp
886+
pub fn timestamp(&self) -> &SystemTime {
887+
self.signed_invoice.raw_invoice().data.timestamp.as_time()
793888
}
794889

795890
/// Returns an iterator over all tagged fields of this Invoice.
@@ -967,6 +1062,9 @@ pub enum CreationError {
9671062

9681063
/// The specified route has too many hops and can't be encoded
9691064
RouteTooLong,
1065+
1066+
/// The unix timestamp of the supplied date is <0 or can't be represented as `SystemTime`
1067+
TimestampOutOfBounds,
9701068
}
9711069

9721070
/// Errors that may occur when converting a `RawInvoice` to an `Invoice`. They relate to the
@@ -997,9 +1095,23 @@ mod test {
9971095
use bitcoin_hashes::hex::FromHex;
9981096
use bitcoin_hashes::sha256::Sha256Hash;
9991097

1098+
#[test]
1099+
fn test_system_time_bounds_assumptions() {
1100+
use std::time::{Duration, SystemTime, UNIX_EPOCH};
1101+
1102+
/// The upper and lower bounds of `SystemTime` are not part of its public contract and are
1103+
/// platform specific. That's why we have to test if our assumptions regarding these bounds
1104+
/// hold on the target platform.
1105+
///
1106+
/// If this test fails on your platform, please don't use the library and open an issue
1107+
/// instead so we can resolve the situation. Currently this library is tested on:
1108+
/// * Linux (64bit)
1109+
let _ = UNIX_EPOCH + Duration::from_secs(::std::i64::MAX as u64);
1110+
}
1111+
10001112
#[test]
10011113
fn test_calc_invoice_hash() {
1002-
use ::{RawInvoice, RawHrp, RawDataPart, Currency};
1114+
use ::{RawInvoice, RawHrp, RawDataPart, Currency, PositiveTimestamp};
10031115
use secp256k1::*;
10041116
use ::TaggedField::*;
10051117

@@ -1010,7 +1122,7 @@ mod test {
10101122
si_prefix: None,
10111123
},
10121124
data: RawDataPart {
1013-
timestamp: 1496314658,
1125+
timestamp: PositiveTimestamp::from_unix_timestamp(1496314658).unwrap(),
10141126
tagged_fields: vec![
10151127
PaymentHash(::Sha256(Sha256Hash::from_hex(
10161128
"0001020304050607080900010203040506070809000102030405060708090102"
@@ -1036,7 +1148,8 @@ mod test {
10361148
use TaggedField::*;
10371149
use secp256k1::{RecoveryId, RecoverableSignature, Secp256k1};
10381150
use secp256k1::key::{SecretKey, PublicKey};
1039-
use {SignedRawInvoice, Signature, RawInvoice, RawHrp, RawDataPart, Currency, Sha256};
1151+
use {SignedRawInvoice, Signature, RawInvoice, RawHrp, RawDataPart, Currency, Sha256,
1152+
PositiveTimestamp};
10401153

10411154
let mut invoice = SignedRawInvoice {
10421155
raw_invoice: RawInvoice {
@@ -1046,7 +1159,7 @@ mod test {
10461159
si_prefix: None,
10471160
},
10481161
data: RawDataPart {
1049-
timestamp: 1496314658,
1162+
timestamp: PositiveTimestamp::from_unix_timestamp(1496314658).unwrap(),
10501163
tagged_fields: vec ! [
10511164
PaymentHash(Sha256(Sha256Hash::from_hex(
10521165
"0001020304050607080900010203040506070809000102030405060708090102"
@@ -1181,6 +1294,7 @@ mod test {
11811294
use ::*;
11821295
use secp256k1::Secp256k1;
11831296
use secp256k1::key::{SecretKey, PublicKey};
1297+
use std::time::UNIX_EPOCH;
11841298

11851299
let secp_ctx = Secp256k1::new();
11861300

@@ -1230,7 +1344,7 @@ mod test {
12301344

12311345
let builder = InvoiceBuilder::new(Currency::BitcoinTestnet)
12321346
.amount_pico_btc(123)
1233-
.timestamp(1234567)
1347+
.timestamp_raw(1234567)
12341348
.payee_pub_key(public_key.clone())
12351349
.expiry_time_seconds(54321)
12361350
.min_final_cltv_expiry(144)
@@ -1250,7 +1364,10 @@ mod test {
12501364

12511365
assert_eq!(invoice.amount_pico_btc(), Some(123));
12521366
assert_eq!(invoice.currency(), Currency::BitcoinTestnet);
1253-
assert_eq!(invoice.timestamp(), 1234567);
1367+
assert_eq!(
1368+
invoice.timestamp().duration_since(UNIX_EPOCH).unwrap().as_secs(),
1369+
1234567
1370+
);
12541371
assert_eq!(invoice.payee_pub_key(), Some(&PayeePubKey(public_key)));
12551372
assert_eq!(invoice.expiry_time(), Some(&ExpiryTime{seconds: 54321}));
12561373
assert_eq!(invoice.min_final_cltv_expiry(), Some(&MinFinalCltvExpiry(144)));

src/ser.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ impl ToBase32<Vec<u5>> for RawDataPart {
119119
let mut encoded = Vec::<u5>::new();
120120

121121
// encode timestamp
122-
encoded.extend(&encode_int_be_base32(self.timestamp));
122+
encoded.extend(self.timestamp.to_base32());
123123

124124
// encode tagged fields
125125
for tagged_field in self.tagged_fields.iter() {
@@ -130,6 +130,12 @@ impl ToBase32<Vec<u5>> for RawDataPart {
130130
}
131131
}
132132

133+
impl ToBase32<Vec<u5>> for PositiveTimestamp {
134+
fn to_base32(&self) -> Vec<u5> {
135+
encode_int_be_base32(self.as_unix_timestamp())
136+
}
137+
}
138+
133139
impl ToBase32<Vec<u5>> for RawTaggedField {
134140
fn to_base32(&self) -> Vec<u5> {
135141
match *self {

tests/ser_de.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ fn get_test_tuples() -> Vec<(String, SignedRawInvoice, Option<SemanticError>)> {
1616
wd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq8rkx3yf5tcsyz3d73gafnh3cax9rn449d9p5uxz9\
1717
ezhhypd0elx87sjle52x86fux2ypatgddc6k63n7erqz25le42c4u4ecky03ylcqca784w".to_owned(),
1818
InvoiceBuilder::new(Currency::Bitcoin)
19-
.timestamp(1496314658)
19+
.timestamp_raw(1496314658)
2020
.payment_hash(Sha256Hash::from_hex(
2121
"0001020304050607080900010203040506070809000102030405060708090102"
2222
).unwrap())
@@ -45,7 +45,7 @@ fn get_test_tuples() -> Vec<(String, SignedRawInvoice, Option<SemanticError>)> {
4545
9zw97j25emudupq63nyw24cg27h2rspfj9srp".to_owned(),
4646
InvoiceBuilder::new(Currency::Bitcoin)
4747
.amount_pico_btc(2500000000)
48-
.timestamp(1496314658)
48+
.timestamp_raw(1496314658)
4949
.payment_hash(Sha256Hash::from_hex(
5050
"0001020304050607080900010203040506070809000102030405060708090102"
5151
).unwrap())
@@ -75,7 +75,7 @@ fn get_test_tuples() -> Vec<(String, SignedRawInvoice, Option<SemanticError>)> {
7575
hhr8wpald05e92xw006sq94mg8v2ndf4sefvf9sygkshp5zfem29trqq2yxxz7".to_owned(),
7676
InvoiceBuilder::new(Currency::Bitcoin)
7777
.amount_pico_btc(20000000000)
78-
.timestamp(1496314658)
78+
.timestamp_raw(1496314658)
7979
.payment_hash(Sha256Hash::from_hex(
8080
"0001020304050607080900010203040506070809000102030405060708090102"
8181
).unwrap())

0 commit comments

Comments
 (0)