Skip to content

Commit 1eb8621

Browse files
committed
Make Amount construction API safe (by checking its <= 21M BTC)
1 parent 1fe5f9e commit 1eb8621

File tree

4 files changed

+161
-101
lines changed

4 files changed

+161
-101
lines changed

src/amount.rs

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,15 @@ impl fmt::Debug for Amount {
2323
}
2424
}
2525

26+
const MAX_MSATS: u64 = 21_000_000_0000_0000_000;
27+
2628
impl Amount {
29+
/// The maximum possible [`Amount`], equal to 21 million BTC
30+
pub const MAX: Amount = Amount(MAX_MSATS);
31+
32+
/// Zero milli-satoshis
33+
pub const ZERO: Amount = Amount(0);
34+
2735
/// The amount in milli-satoshis
2836
#[inline]
2937
pub const fn milli_sats(&self) -> u64 {
@@ -48,19 +56,34 @@ impl Amount {
4856

4957
/// Constructs a new [`Amount`] for the given number of milli-satoshis.
5058
#[inline]
51-
pub const fn from_milli_sats(msats: u64) -> Self {
52-
Amount(msats)
59+
pub const fn from_milli_sats(msats: u64) -> Result<Self, ()> {
60+
if msats > MAX_MSATS {
61+
Err(())
62+
} else {
63+
Ok(Amount(msats))
64+
}
5365
}
5466

5567
/// Constructs a new [`Amount`] for the given number of satoshis.
5668
#[inline]
57-
pub const fn from_sats(sats: u64) -> Self {
58-
Amount(sats * 1000)
69+
pub const fn from_sats(sats: u64) -> Result<Self, ()> {
70+
Self::from_milli_sats(sats.saturating_mul(1000))
71+
}
72+
73+
/// Constructs a new [`Amount`] for the given number of satoshis, panicking if the amount is
74+
/// too large.
75+
pub(crate) const fn from_sats_panicy(sats: u64) -> Self {
76+
let amt = sats.saturating_mul(1000);
77+
if amt > MAX_MSATS {
78+
panic!("Sats value greater than 21 million Bitcoin");
79+
} else {
80+
Amount(amt)
81+
}
5982
}
6083

6184
/// Adds an [`Amount`] to this [`Amount`], saturating to avoid overflowing 21 million bitcoin.
6285
#[inline]
63-
pub fn saturating_add(self, rhs: Amount) -> Amount {
86+
pub const fn saturating_add(self, rhs: Amount) -> Amount {
6487
match self.0.checked_add(rhs.0) {
6588
Some(amt) if amt <= 21_000_000_0000_0000_000 => Amount(amt),
6689
_ => Amount(21_000_000_0000_0000_000),
@@ -69,7 +92,7 @@ impl Amount {
6992

7093
/// Subtracts an [`Amount`] from this [`Amount`], saturating to avoid underflowing.
7194
#[inline]
72-
pub fn saturating_sub(self, rhs: Amount) -> Amount {
95+
pub const fn saturating_sub(self, rhs: Amount) -> Amount {
7396
Amount(self.0.saturating_sub(rhs.0))
7497
}
7598

@@ -120,15 +143,15 @@ mod test {
120143
#[test]
121144
#[rustfmt::skip]
122145
fn test_display() {
123-
assert_eq!(Amount::from_milli_sats(0).btc_decimal_rounding_up_to_sats().to_string(), "0");
124-
assert_eq!(Amount::from_milli_sats(1).btc_decimal_rounding_up_to_sats().to_string(), "0.00000001");
125-
assert_eq!(Amount::from_sats(1).btc_decimal_rounding_up_to_sats().to_string(), "0.00000001");
126-
assert_eq!(Amount::from_sats(10).btc_decimal_rounding_up_to_sats().to_string(), "0.0000001");
127-
assert_eq!(Amount::from_sats(15).btc_decimal_rounding_up_to_sats().to_string(), "0.00000015");
128-
assert_eq!(Amount::from_sats(1_0000).btc_decimal_rounding_up_to_sats().to_string(), "0.0001");
129-
assert_eq!(Amount::from_sats(1_2345).btc_decimal_rounding_up_to_sats().to_string(), "0.00012345");
130-
assert_eq!(Amount::from_sats(1_2345_6789).btc_decimal_rounding_up_to_sats().to_string(), "1.23456789");
131-
assert_eq!(Amount::from_sats(1_0000_0000).btc_decimal_rounding_up_to_sats().to_string(), "1");
132-
assert_eq!(Amount::from_sats(5_0000_0000).btc_decimal_rounding_up_to_sats().to_string(), "5");
146+
assert_eq!(Amount::from_milli_sats(0).unwrap().btc_decimal_rounding_up_to_sats().to_string(), "0");
147+
assert_eq!(Amount::from_milli_sats(1).unwrap().btc_decimal_rounding_up_to_sats().to_string(), "0.00000001");
148+
assert_eq!(Amount::from_sats(1).unwrap().btc_decimal_rounding_up_to_sats().to_string(), "0.00000001");
149+
assert_eq!(Amount::from_sats(10).unwrap().btc_decimal_rounding_up_to_sats().to_string(), "0.0000001");
150+
assert_eq!(Amount::from_sats(15).unwrap().btc_decimal_rounding_up_to_sats().to_string(), "0.00000015");
151+
assert_eq!(Amount::from_sats(1_0000).unwrap().btc_decimal_rounding_up_to_sats().to_string(), "0.0001");
152+
assert_eq!(Amount::from_sats(1_2345).unwrap().btc_decimal_rounding_up_to_sats().to_string(), "0.00012345");
153+
assert_eq!(Amount::from_sats(1_2345_6789).unwrap().btc_decimal_rounding_up_to_sats().to_string(), "1.23456789");
154+
assert_eq!(Amount::from_sats(1_0000_0000).unwrap().btc_decimal_rounding_up_to_sats().to_string(), "1");
155+
assert_eq!(Amount::from_sats(5_0000_0000).unwrap().btc_decimal_rounding_up_to_sats().to_string(), "5");
133156
}
134157
}

src/dns_resolver.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ mod tests {
7474
assert_eq!(hrn.user(), "send.some");
7575
assert_eq!(hrn.domain(), "satsto.me");
7676

77-
instr.set_amount(Amount::from_sats(100_000), &resolver).await.unwrap()
77+
instr.set_amount(Amount::from_sats(100_000).unwrap(), &resolver).await.unwrap()
7878
} else {
7979
panic!();
8080
};

src/http_resolver.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,9 @@ impl HTTPHrnResolver {
157157
}
158158
let expected_description_hash = Sha256::hash(init.metadata.as_bytes()).to_byte_array();
159159
Ok(HrnResolution::LNURLPay {
160-
min_value: Amount::from_milli_sats(init.min_sendable),
161-
max_value: Amount::from_milli_sats(init.max_sendable),
160+
min_value: Amount::from_milli_sats(init.min_sendable)
161+
.map_err(|_| "LNURL initial response had a minimum amount greater than 21M BTC")?,
162+
max_value: Amount::from_milli_sats(init.max_sendable).unwrap_or(Amount::MAX),
162163
callback: init.callback,
163164
expected_description_hash,
164165
recipient_description,
@@ -272,7 +273,7 @@ mod tests {
272273
assert_eq!(hrn.user(), "send.some");
273274
assert_eq!(hrn.domain(), "satsto.me");
274275

275-
instr.set_amount(Amount::from_sats(100_000), &resolver).await.unwrap()
276+
instr.set_amount(Amount::from_sats(100_000).unwrap(), &resolver).await.unwrap()
276277
} else {
277278
panic!();
278279
};
@@ -316,7 +317,7 @@ mod tests {
316317
assert_eq!(hrn.user(), "lnurltest");
317318
assert_eq!(hrn.domain(), "bitcoin.ninja");
318319

319-
instr.set_amount(Amount::from_sats(100_000), &HTTPHrnResolver).await.unwrap()
320+
instr.set_amount(Amount::from_sats(100_000).unwrap(), &HTTPHrnResolver).await.unwrap()
320321
} else {
321322
panic!();
322323
};

0 commit comments

Comments
 (0)