Skip to content

Commit d517ae0

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

File tree

4 files changed

+162
-101
lines changed

4 files changed

+162
-101
lines changed

src/amount.rs

Lines changed: 43 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 {
@@ -47,20 +55,39 @@ impl Amount {
4755
}
4856

4957
/// Constructs a new [`Amount`] for the given number of milli-satoshis.
58+
///
59+
/// Fails only if `msats` is greater than 21 million Bitcoin (in milli-satoshis).
5060
#[inline]
51-
pub const fn from_milli_sats(msats: u64) -> Self {
52-
Amount(msats)
61+
pub const fn from_milli_sats(msats: u64) -> Result<Self, ()> {
62+
if msats > MAX_MSATS {
63+
Err(())
64+
} else {
65+
Ok(Amount(msats))
66+
}
5367
}
5468

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

6188
/// Adds an [`Amount`] to this [`Amount`], saturating to avoid overflowing 21 million bitcoin.
6289
#[inline]
63-
pub fn saturating_add(self, rhs: Amount) -> Amount {
90+
pub const fn saturating_add(self, rhs: Amount) -> Amount {
6491
match self.0.checked_add(rhs.0) {
6592
Some(amt) if amt <= 21_000_000_0000_0000_000 => Amount(amt),
6693
_ => Amount(21_000_000_0000_0000_000),
@@ -69,7 +96,7 @@ impl Amount {
6996

7097
/// Subtracts an [`Amount`] from this [`Amount`], saturating to avoid underflowing.
7198
#[inline]
72-
pub fn saturating_sub(self, rhs: Amount) -> Amount {
99+
pub const fn saturating_sub(self, rhs: Amount) -> Amount {
73100
Amount(self.0.saturating_sub(rhs.0))
74101
}
75102

@@ -120,15 +147,15 @@ mod test {
120147
#[test]
121148
#[rustfmt::skip]
122149
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");
150+
assert_eq!(Amount::from_milli_sats(0).unwrap().btc_decimal_rounding_up_to_sats().to_string(), "0");
151+
assert_eq!(Amount::from_milli_sats(1).unwrap().btc_decimal_rounding_up_to_sats().to_string(), "0.00000001");
152+
assert_eq!(Amount::from_sats(1).unwrap().btc_decimal_rounding_up_to_sats().to_string(), "0.00000001");
153+
assert_eq!(Amount::from_sats(10).unwrap().btc_decimal_rounding_up_to_sats().to_string(), "0.0000001");
154+
assert_eq!(Amount::from_sats(15).unwrap().btc_decimal_rounding_up_to_sats().to_string(), "0.00000015");
155+
assert_eq!(Amount::from_sats(1_0000).unwrap().btc_decimal_rounding_up_to_sats().to_string(), "0.0001");
156+
assert_eq!(Amount::from_sats(1_2345).unwrap().btc_decimal_rounding_up_to_sats().to_string(), "0.00012345");
157+
assert_eq!(Amount::from_sats(1_2345_6789).unwrap().btc_decimal_rounding_up_to_sats().to_string(), "1.23456789");
158+
assert_eq!(Amount::from_sats(1_0000_0000).unwrap().btc_decimal_rounding_up_to_sats().to_string(), "1");
159+
assert_eq!(Amount::from_sats(5_0000_0000).unwrap().btc_decimal_rounding_up_to_sats().to_string(), "5");
133160
}
134161
}

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)