Skip to content

Commit 449e4a6

Browse files
committed
improvement(settlement): return Error on overflow during conversion
- If exchange rate normalization fails return a reject packet - In all other places we use BigUint so the conversion should never fail, but we still handle the error graciously - prefer shadwing of variable name to avoid using the wrong variable - clarify comment about SettlementEngineDetails
1 parent 1c3a182 commit 449e4a6

File tree

4 files changed

+179
-67
lines changed

4 files changed

+179
-67
lines changed

crates/interledger-service-util/src/exchange_rates_service.rs

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,32 @@ where
9898
from: request.from.asset_scale(),
9999
to: request.to.asset_scale(),
100100
});
101-
let outgoing_amount = (request.prepare.amount() as f64) * scaled_rate;
102-
request.prepare.set_amount(outgoing_amount as u64);
103-
trace!("Converted incoming amount of: {} {} (scale {}) from account {} to outgoing amount of: {} {} (scale {}) for account {}", request.original_amount, request.from.asset_code(), request.from.asset_scale(), request.from.id(), outgoing_amount, request.to.asset_code(), request.to.asset_scale(), request.to.id());
101+
102+
match scaled_rate {
103+
Ok(scaled_rate) => {
104+
let outgoing_amount = (request.prepare.amount() as f64) * scaled_rate;
105+
request.prepare.set_amount(outgoing_amount as u64);
106+
trace!("Converted incoming amount of: {} {} (scale {}) from account {} to outgoing amount of: {} {} (scale {}) for account {}",
107+
request.original_amount, request.from.asset_code(), request.from.asset_scale(), request.from.id(),
108+
outgoing_amount, request.to.asset_code(), request.to.asset_scale(), request.to.id());
109+
}
110+
Err(_) => {
111+
return Box::new(err(RejectBuilder {
112+
code: ErrorCode::F02_UNREACHABLE,
113+
message: format!(
114+
"Could not convert exchange rate from {}:{} to: {}:{}",
115+
request.from.asset_code(),
116+
request.from.asset_scale(),
117+
request.to.asset_code(),
118+
request.to.asset_scale(),
119+
)
120+
.as_bytes(),
121+
triggered_by: Some(&self.ilp_address),
122+
data: &[],
123+
}
124+
.build()));
125+
}
126+
}
104127
}
105128

106129
Box::new(self.next.send_request(request))

crates/interledger-settlement-engines/src/engines/ethereum_ledger/eth_engine.rs

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -761,7 +761,7 @@ where
761761
let engine_scale = self.asset_scale;
762762
Box::new(
763763
result(BigUint::from_str(&body.amount).map_err(move |err| {
764-
let error_msg = format!("Error converting to U256 {:?}", err);
764+
let error_msg = format!("Error converting to BigUint {:?}", err);
765765
error!("{:?}", error_msg);
766766
(StatusCode::from_u16(400).unwrap(), error_msg)
767767
}))
@@ -773,13 +773,21 @@ where
773773
to: engine_scale,
774774
});
775775

776-
// Typecast from num_bigint::BigUInt because we're using
777-
// ethereum_types::U256 for all rust-web3 related functionality
778-
result(U256::from_dec_str(&amount.to_string()).map_err(move |err| {
779-
let error_msg = format!("Error converting to U256 {:?}", err);
780-
error!("{:?}", error_msg);
781-
(StatusCode::from_u16(400).unwrap(), error_msg)
782-
}))
776+
result(amount)
777+
.map_err(move |err| {
778+
let error_msg = format!("Error scaling amount: {:?}", err);
779+
error!("{:?}", error_msg);
780+
(StatusCode::from_u16(400).unwrap(), error_msg)
781+
})
782+
.and_then(move |amount| {
783+
// Typecast from num_bigint::BigUInt because we're using
784+
// ethereum_types::U256 for all rust-web3 related functionality
785+
result(U256::from_dec_str(&amount.to_string()).map_err(move |err| {
786+
let error_msg = format!("Error converting to U256 {:?}", err);
787+
error!("{:?}", error_msg);
788+
(StatusCode::from_u16(400).unwrap(), error_msg)
789+
}))
790+
})
783791
})
784792
.and_then(move |amount| {
785793
self_clone

crates/interledger-settlement/src/api.rs

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -190,23 +190,31 @@ impl_web! {
190190
from: engine_scale,
191191
to: account.asset_scale(),
192192
});
193-
// If we'd overflow, settle for the maximum u64 value
194-
let safe_amount = if let Some(amount_u64) = amount.to_u64() {
195-
amount_u64
196-
} else {
197-
debug!("Amount settled from engine overflowed during conversion to connector scale: {}. Settling for u64::MAX", amount);
198-
std::u64::MAX
199-
};
200-
store.update_balance_for_incoming_settlement(account_id, safe_amount, idempotency_key)
193+
result(amount.clone())
201194
.map_err(move |_| {
202-
let error_msg = format!("Error updating balance of account: {} for incoming settlement of amount: {}", account_id, amount);
195+
let error_msg = format!("Could not convert amount: {:?}", amount);
203196
error!("{}", error_msg);
204197
(StatusCode::from_u16(500).unwrap(), error_msg)
205198
})
206-
.and_then(move |_| {
207-
// TODO: Return a Quantity of safe_amount and account.asset_scale()
208-
let ret = Bytes::from("Success");
209-
Ok((StatusCode::OK, ret))
199+
.and_then(move |amount| {
200+
// If we'd overflow, settle for the maximum u64 value
201+
let amount = if let Some(amount_u64) = amount.to_u64() {
202+
amount_u64
203+
} else {
204+
debug!("Amount settled from engine overflowed during conversion to connector scale: {:?}. Settling for u64::MAX", amount);
205+
std::u64::MAX
206+
};
207+
store.update_balance_for_incoming_settlement(account_id, amount, idempotency_key)
208+
.map_err(move |_| {
209+
let error_msg = format!("Error updating balance of account: {} for incoming settlement of amount: {}", account_id, amount);
210+
error!("{}", error_msg);
211+
(StatusCode::from_u16(500).unwrap(), error_msg)
212+
})
213+
.and_then(move |_| {
214+
// TODO: Return a Quantity of amount and account.asset_scale()
215+
let ret = Bytes::from("Success");
216+
Ok((StatusCode::OK, ret))
217+
})
210218
})
211219
})
212220
}))

crates/interledger-settlement/src/lib.rs

Lines changed: 116 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,7 @@ mod fixtures;
1919
mod message_service;
2020
#[cfg(test)]
2121
mod test_helpers;
22-
use log::debug;
2322
use num_bigint::BigUint;
24-
use num_traits::cast::ToPrimitive;
2523
use std::ops::{Div, Mul};
2624

2725
pub use api::SettlementApi;
@@ -47,6 +45,10 @@ impl Quantity {
4745
}
4846
}
4947

48+
// TODO: Since we still haven't finalized all the settlement details, we might
49+
// end up deciding to add some more values, e.g. some settlement engine uid or similar.
50+
// All instances of this struct should be replaced with Url instances once/if we
51+
// agree that there is no more info required to refer to an engine.
5052
pub struct SettlementEngineDetails {
5153
/// Base URL of the settlement engine
5254
pub url: Url,
@@ -104,53 +106,61 @@ pub struct ConvertDetails {
104106

105107
/// Traits for u64 and f64 asset code conversions for amounts and rates
106108
pub trait Convert {
107-
fn normalize_scale(&self, details: ConvertDetails) -> Self;
109+
type Item: Sized;
110+
111+
// Returns the scaled result, or an error if there was an overflow
112+
fn normalize_scale(&self, details: ConvertDetails) -> Result<Self::Item, ()>;
108113
}
109114

110115
impl Convert for u64 {
111-
fn normalize_scale(&self, details: ConvertDetails) -> Self {
116+
type Item = u64;
117+
118+
fn normalize_scale(&self, details: ConvertDetails) -> Result<Self::Item, ()> {
112119
let scale_diff = (details.from as i8 - details.to as i8).abs() as u8;
113120
let scale = 10u64.pow(scale_diff.into());
114-
let num = BigUint::from(*self);
115-
let num = if details.to >= details.from {
116-
num.mul(scale)
121+
let (res, overflow) = if details.to >= details.from {
122+
self.overflowing_mul(scale)
117123
} else {
118-
num.div(scale)
124+
self.overflowing_div(scale)
119125
};
120-
if let Some(num_u64) = num.to_u64() {
121-
num_u64
126+
if overflow {
127+
Err(())
122128
} else {
123-
debug!(
124-
"Overflow during conversion from {} {:?}. Using u64::MAX",
125-
num, details
126-
);
127-
std::u64::MAX
129+
Ok(res)
128130
}
129131
}
130132
}
131133

132134
impl Convert for f64 {
135+
type Item = f64;
133136
// Not overflow safe. Would require using a package for Big floating point
134137
// numbers such as BigDecimal
135-
fn normalize_scale(&self, details: ConvertDetails) -> Self {
138+
fn normalize_scale(&self, details: ConvertDetails) -> Result<Self::Item, ()> {
136139
let scale_diff = (details.from as i8 - details.to as i8).abs() as u8;
137140
let scale = 10f64.powi(scale_diff.into());
138-
if details.to >= details.from {
141+
let res = if details.to >= details.from {
139142
self * scale
140143
} else {
141144
self / scale
145+
};
146+
if res == std::f64::INFINITY {
147+
Err(())
148+
} else {
149+
Ok(res)
142150
}
143151
}
144152
}
145153

146154
impl Convert for BigUint {
147-
fn normalize_scale(&self, details: ConvertDetails) -> Self {
155+
type Item = BigUint;
156+
157+
fn normalize_scale(&self, details: ConvertDetails) -> Result<Self::Item, ()> {
148158
let scale_diff = (details.from as i8 - details.to as i8).abs() as u8;
149159
let scale = 10u64.pow(scale_diff.into());
150160
if details.to >= details.from {
151-
self.mul(scale)
161+
Ok(self.mul(scale))
152162
} else {
153-
self.div(scale)
163+
Ok(self.div(scale))
154164
}
155165
}
156166
}
@@ -162,93 +172,156 @@ mod tests {
162172

163173
#[test]
164174
fn u64_test() {
165-
// does not overflow
175+
// overflows
166176
let huge_number = std::u64::MAX / 10;
167177
assert_eq!(
168-
huge_number.normalize_scale(ConvertDetails { from: 1, to: 18 }),
169-
std::u64::MAX
178+
huge_number
179+
.normalize_scale(ConvertDetails { from: 1, to: 18 })
180+
.unwrap_err(),
181+
(),
170182
);
171183
// 1 unit with scale 1, is 1 unit with scale 1
172-
assert_eq!(1u64.normalize_scale(ConvertDetails { from: 1, to: 1 }), 1);
184+
assert_eq!(
185+
1u64.normalize_scale(ConvertDetails { from: 1, to: 1 })
186+
.unwrap(),
187+
1
188+
);
173189
// there's leftovers for all number slots which do not increase in
174190
// increments of 10^abs(to_scale-from_scale)
175-
assert_eq!(1u64.normalize_scale(ConvertDetails { from: 2, to: 1 }), 0);
191+
assert_eq!(
192+
1u64.normalize_scale(ConvertDetails { from: 2, to: 1 })
193+
.unwrap(),
194+
0
195+
);
176196
// 1 unit with scale 1, is 10 units with scale 2
177-
assert_eq!(1u64.normalize_scale(ConvertDetails { from: 1, to: 2 }), 10);
197+
assert_eq!(
198+
1u64.normalize_scale(ConvertDetails { from: 1, to: 2 })
199+
.unwrap(),
200+
10
201+
);
178202
// 1 gwei (scale 9) is 1e9 wei (scale 18)
179203
assert_eq!(
180-
1u64.normalize_scale(ConvertDetails { from: 9, to: 18 }),
204+
1u64.normalize_scale(ConvertDetails { from: 9, to: 18 })
205+
.unwrap(),
181206
1_000_000_000
182207
);
183208
// 1_000_000_000 wei is 1gwei
184209
assert_eq!(
185-
1_000_000_000u64.normalize_scale(ConvertDetails { from: 18, to: 9 }),
210+
1_000_000_000u64
211+
.normalize_scale(ConvertDetails { from: 18, to: 9 })
212+
.unwrap(),
186213
1,
187214
);
188215
// 10 units with base 2 is 100 units with base 3
189216
assert_eq!(
190-
10u64.normalize_scale(ConvertDetails { from: 2, to: 3 }),
217+
10u64
218+
.normalize_scale(ConvertDetails { from: 2, to: 3 })
219+
.unwrap(),
191220
100
192221
);
193222
// 299 units with base 3 is 29 units with base 2 (0.9 leftovers)
194223
assert_eq!(
195-
299u64.normalize_scale(ConvertDetails { from: 3, to: 2 }),
224+
299u64
225+
.normalize_scale(ConvertDetails { from: 3, to: 2 })
226+
.unwrap(),
196227
29
197228
);
198-
assert_eq!(999u64.normalize_scale(ConvertDetails { from: 9, to: 6 }), 0);
199229
assert_eq!(
200-
1000u64.normalize_scale(ConvertDetails { from: 9, to: 6 }),
230+
999u64
231+
.normalize_scale(ConvertDetails { from: 9, to: 6 })
232+
.unwrap(),
233+
0
234+
);
235+
assert_eq!(
236+
1000u64
237+
.normalize_scale(ConvertDetails { from: 9, to: 6 })
238+
.unwrap(),
201239
1
202240
);
203241
assert_eq!(
204-
1999u64.normalize_scale(ConvertDetails { from: 9, to: 6 }),
242+
1999u64
243+
.normalize_scale(ConvertDetails { from: 9, to: 6 })
244+
.unwrap(),
205245
1
206246
);
207247
}
208248

209249
#[allow(clippy::float_cmp)]
210250
#[test]
211251
fn f64_test() {
252+
// overflow
253+
assert_eq!(
254+
std::f64::MAX
255+
.normalize_scale(ConvertDetails {
256+
from: 1,
257+
to: std::u8::MAX,
258+
})
259+
.unwrap_err(),
260+
()
261+
);
262+
212263
// 1 unit with base 1, is 1 unit with base 1
213-
assert_eq!(1f64.normalize_scale(ConvertDetails { from: 1, to: 1 }), 1.0);
264+
assert_eq!(
265+
1f64.normalize_scale(ConvertDetails { from: 1, to: 1 })
266+
.unwrap(),
267+
1.0
268+
);
214269
// 1 unit with base 10, is 10 units with base 1
215270
assert_eq!(
216-
1f64.normalize_scale(ConvertDetails { from: 1, to: 2 }),
271+
1f64.normalize_scale(ConvertDetails { from: 1, to: 2 })
272+
.unwrap(),
217273
10.0
218274
);
219275
// 1 sat is 1e9 wei (multiplied by rate)
220276
assert_eq!(
221-
1f64.normalize_scale(ConvertDetails { from: 9, to: 18 }),
277+
1f64.normalize_scale(ConvertDetails { from: 9, to: 18 })
278+
.unwrap(),
222279
1_000_000_000.0
223280
);
224281

225282
// 1.0 unit with base 2 is 0.1 unit with base 1
226-
assert_eq!(1f64.normalize_scale(ConvertDetails { from: 2, to: 1 }), 0.1);
227283
assert_eq!(
228-
10.5f64.normalize_scale(ConvertDetails { from: 2, to: 1 }),
284+
1f64.normalize_scale(ConvertDetails { from: 2, to: 1 })
285+
.unwrap(),
286+
0.1
287+
);
288+
assert_eq!(
289+
10.5f64
290+
.normalize_scale(ConvertDetails { from: 2, to: 1 })
291+
.unwrap(),
229292
1.05
230293
);
231294
// 100 units with base 3 is 10 units with base 2
232295
assert_eq!(
233-
100f64.normalize_scale(ConvertDetails { from: 3, to: 2 }),
296+
100f64
297+
.normalize_scale(ConvertDetails { from: 3, to: 2 })
298+
.unwrap(),
234299
10.0
235300
);
236301
// 299 units with base 3 is 29.9 with base 2
237302
assert_eq!(
238-
299f64.normalize_scale(ConvertDetails { from: 3, to: 2 }),
303+
299f64
304+
.normalize_scale(ConvertDetails { from: 3, to: 2 })
305+
.unwrap(),
239306
29.9
240307
);
241308

242309
assert_eq!(
243-
999f64.normalize_scale(ConvertDetails { from: 9, to: 6 }),
310+
999f64
311+
.normalize_scale(ConvertDetails { from: 9, to: 6 })
312+
.unwrap(),
244313
0.999
245314
);
246315
assert_eq!(
247-
1000f64.normalize_scale(ConvertDetails { from: 9, to: 6 }),
316+
1000f64
317+
.normalize_scale(ConvertDetails { from: 9, to: 6 })
318+
.unwrap(),
248319
1.0
249320
);
250321
assert_eq!(
251-
1999f64.normalize_scale(ConvertDetails { from: 9, to: 6 }),
322+
1999f64
323+
.normalize_scale(ConvertDetails { from: 9, to: 6 })
324+
.unwrap(),
252325
1.999
253326
);
254327
}

0 commit comments

Comments
 (0)