Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Asset Scale conversions #192

Merged
merged 22 commits into from Aug 9, 2019
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
ba54167
fix(settlement-client): do not convert scale
gakonst Aug 5, 2019
5e576ab
feat(settlement): Implement asset scale conversion for u256
gakonst Aug 5, 2019
a04832a
feat(settlement): Normalize the amount properly when being notified b…
gakonst Aug 5, 2019
4fc57a2
fix(eth-se): Scale the amount to settle for based on the body and eng…
gakonst Aug 5, 2019
f02937d
improve tester
gakonst Aug 5, 2019
9395f53
test(eth-se): adjust test for proper scale conversion
gakonst Aug 5, 2019
0041bd7
test(eth-xrp) Set ETHXRP exchange rate
gakonst Aug 5, 2019
c32f27c
test(eth-xrp): Switch back to BTP
gakonst Aug 5, 2019
5b1e2a5
fix(crate): Remove settlement_engine_asset_scale from account
gakonst Aug 5, 2019
a5874a9
improvement(settlement/engines): use BigUInt to handle big numbers
gakonst Aug 6, 2019
61ecec5
test(settlement-engines): use asset_scale local variables per test
gakonst Aug 6, 2019
642a29b
test(engine-eth-xrp): elaborate on how results are produced
gakonst Aug 6, 2019
725b9f0
fix(settlement): Convert asset scale properly
gakonst Aug 6, 2019
ca46d74
fix comment
gakonst Aug 6, 2019
56237a6
Revert "test(eth-xrp): Switch back to BTP"
gakonst Aug 6, 2019
1f6f9a4
feat(settlement/engine): Return None when idempotent data not found i…
gakonst Aug 6, 2019
9de2c7f
docs: fix readme in interoperability test
gakonst Aug 6, 2019
b5b62ec
fix(settlement): Make u64 conversions overflow-safe, and warn for f64…
gakonst Aug 8, 2019
1c3a182
docs: fix review comments
gakonst Aug 8, 2019
449e4a6
improvement(settlement): return Error on overflow during conversion
gakonst Aug 8, 2019
d227963
improvement(exchange-rate): make the scale conversion after the excha…
gakonst Aug 8, 2019
fa69cd6
test(exchange-rate): Add tests which verify that rate conversions fai…
gakonst Aug 9, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 0 additions & 1 deletion crates/interledger-api/src/lib.rs
Expand Up @@ -71,7 +71,6 @@ pub struct AccountDetails {
pub amount_per_minute_limit: Option<u64>,
pub packets_per_minute_limit: Option<u32>,
pub settlement_engine_url: Option<String>,
pub settlement_engine_asset_scale: Option<u8>,
}

pub struct NodeApi<S, I> {
Expand Down
8 changes: 6 additions & 2 deletions crates/interledger-service-util/src/exchange_rates_service.rs
Expand Up @@ -90,9 +90,13 @@ where
.build()));
};

// Note that this is not an overflow safe operation. Given realistic
emschwartz marked this conversation as resolved.
Show resolved Hide resolved
// assumptions, the asset scale will be <=18 and >=1.
// It is doubtful that the exchange rate between two assets,
// multiplied by 18 would exceed std::f64::MAX
let scaled_rate = rate.normalize_scale(ConvertDetails {
from: request.to.asset_scale(),
to: request.from.asset_scale(),
from: request.from.asset_scale(),
to: request.to.asset_scale(),
});
let outgoing_amount = (request.prepare.amount() as f64) * scaled_rate;
request.prepare.set_amount(outgoing_amount as u64);
Expand Down
3 changes: 2 additions & 1 deletion crates/interledger-settlement-engines/Cargo.toml
Expand Up @@ -34,6 +34,7 @@ http = "0.1.17"
clap = "2.32.0"
clarity = { git = "https://github.com/gakonst/clarity" }
sha3 = "0.8.2"
num-bigint = "0.2.2"

[dev-dependencies]
lazy_static = "1.3"
Expand All @@ -43,4 +44,4 @@ net2 = "0.2.33"
os_type = "2.2.0"
rand = "0.7.0"
interledger = { path = "../interledger", version = "0.4.0" }
interledger-packet = { path = "../interledger-packet", version = "0.2.1" }
interledger-packet = { path = "../interledger-packet", version = "0.2.1" }
24 changes: 14 additions & 10 deletions crates/interledger-settlement-engines/src/api.rs
Expand Up @@ -7,7 +7,7 @@ use futures::{
use hyper::{Response, StatusCode};
use interledger_settlement::Quantity;
use interledger_settlement::{IdempotentData, IdempotentStore};
use log::trace;
use log::error;
use ring::digest::{digest, SHA256};
use tokio::executor::spawn;
use tower_web::{net::ConnectionStream, ServiceBuilder};
Expand Down Expand Up @@ -77,19 +77,23 @@ impl_web! {
&self,
idempotency_key: String,
input_hash: [u8; 32],
) -> impl Future<Item = (StatusCode, Bytes), Error = String> {
) -> impl Future<Item = Option<(StatusCode, Bytes)>, Error = String> {
self.store
.load_idempotent_data(idempotency_key.clone())
.map_err(move |_| {
let error_msg = "Couldn't load idempotent data".to_owned();
trace!("{}", error_msg);
let error_msg = format!("Couldn't load idempotent data for idempotency key {:?}", idempotency_key);
error!("{}", error_msg);
error_msg
})
.and_then(move |ret: IdempotentData| {
if ret.2 == input_hash {
Ok((ret.0, ret.1))
.and_then(move |ret: Option<IdempotentData>| {
if let Some(ret) = ret {
if ret.2 == input_hash {
Ok(Some((ret.0, ret.1)))
} else {
Ok(Some((StatusCode::from_u16(409).unwrap(), Bytes::from(&b"Provided idempotency key is tied to other input"[..]))))
}
} else {
Ok((StatusCode::from_u16(409).unwrap(), Bytes::from(&b"Provided idempotency key is tied to other input"[..])))
Ok(None)
}
})
}
Expand All @@ -104,8 +108,8 @@ impl_web! {
Either::A(
self.check_idempotency(idempotency_key.clone(), input_hash)
.map_err(|err| Response::builder().status(502).body(err).unwrap())
.then(move |ret: Result<(StatusCode, Bytes), Response<String>>| {
if let Ok(ret) = ret {
.and_then(move |ret: Option<(StatusCode, Bytes)>| {
if let Some(ret) = ret {
let resp = Response::builder().status(ret.0).body(String::from_utf8_lossy(&ret.1).to_string()).unwrap();
if ret.0.is_success() {
return Either::A(Either::A(ok(resp)))
Expand Down
Expand Up @@ -16,6 +16,7 @@ use ethereum_tx_sign::web3::{
use hyper::StatusCode;
use interledger_store_redis::RedisStoreBuilder;
use log::info;
use num_bigint::BigUint;
use redis::IntoConnectionInfo;
use reqwest::r#async::{Client, Response as HttpResponse};
use ring::{digest, hmac};
Expand All @@ -35,7 +36,7 @@ use uuid::Uuid;

use crate::stores::redis_ethereum_ledger::*;
use crate::{ApiResponse, CreateAccount, SettlementEngine, SettlementEngineApi};
use interledger_settlement::Quantity;
use interledger_settlement::{Convert, ConvertDetails, Quantity};

const MAX_RETRIES: usize = 10;
const ETH_CREATE_ACCOUNT_PREFIX: &[u8] = b"ilp-ethl-create-account-message";
Expand Down Expand Up @@ -480,7 +481,7 @@ where
) -> impl Future<Item = (), Error = ()> {
let mut url = self.connector_url.clone();
let account_id_clone = account_id.clone();
let asset_scale = self.asset_scale;
let engine_scale = self.asset_scale;
url.path_segments_mut()
.expect("Invalid connector URL")
.push("accounts")
Expand All @@ -493,7 +494,7 @@ where
client
.post(url.clone())
.header("Idempotency-Key", tx_hash.to_string())
.json(&json!({ "amount": amount.to_string(), "scale" : asset_scale }))
.json(&json!({ "amount": amount.to_string(), "scale" : engine_scale }))
.send()
.map_err(move |err| {
error!(
Expand Down Expand Up @@ -757,12 +758,29 @@ where
body: Quantity,
) -> Box<dyn Future<Item = ApiResponse, Error = ApiResponse> + Send> {
let self_clone = self.clone();
let engine_scale = self.asset_scale;
Box::new(
result(U256::from_dec_str(&body.amount).map_err(move |err| {
result(BigUint::from_str(&body.amount).map_err(move |err| {
let error_msg = format!("Error converting to U256 {:?}", err);
error!("{:?}", error_msg);
(StatusCode::from_u16(400).unwrap(), error_msg)
}))
.and_then(move |amount_from_connector| {
// If we receive a Quantity { amount: "1", scale: 9},
// we must normalize it to our engine's scale
let amount = amount_from_connector.normalize_scale(ConvertDetails {
emschwartz marked this conversation as resolved.
Show resolved Hide resolved
from: body.scale,
to: engine_scale,
});

// Typecast from num_bigint::BigUInt because we're using
// ethereum_types::U256 for all rust-web3 related functionality
result(U256::from_dec_str(&amount.to_string()).map_err(move |err| {
let error_msg = format!("Error converting to U256 {:?}", err);
error!("{:?}", error_msg);
(StatusCode::from_u16(400).unwrap(), error_msg)
}))
})
.and_then(move |amount| {
self_clone
.load_account(account_id)
Expand Down Expand Up @@ -929,7 +947,7 @@ mod tests {

let bob_mock = mockito::mock("POST", "/accounts/42/settlements")
.match_body(mockito::Matcher::JsonString(
"{\"amount\": \"100\", \"scale\": 18 }".to_string(),
"{\"amount\": \"100000000000\", \"scale\": 18 }".to_string(),
))
.with_status(200)
.with_body("OK".to_string())
Expand All @@ -953,7 +971,7 @@ mod tests {
);

let ret =
block_on(alice_engine.send_money(bob.id.to_string(), Quantity::new(100, 6))).unwrap();
block_on(alice_engine.send_money(bob.id.to_string(), Quantity::new(100, 9))).unwrap();
assert_eq!(ret.0.as_u16(), 200);
assert_eq!(ret.1, "OK");

Expand Down
Expand Up @@ -176,14 +176,14 @@ impl IdempotentStore for TestStore {
fn load_idempotent_data(
&self,
idempotency_key: String,
) -> Box<dyn Future<Item = IdempotentData, Error = ()> + Send> {
) -> Box<dyn Future<Item = Option<IdempotentData>, Error = ()> + Send> {
let cache = self.cache.read();
if let Some(data) = cache.get(&idempotency_key) {
let mut guard = self.cache_hits.write();
*guard += 1; // used to test how many times this branch gets executed
Box::new(ok((data.0, Bytes::from(data.1.clone()), data.2)))
Box::new(ok(Some((data.0, Bytes::from(data.1.clone()), data.2))))
} else {
Box::new(err(()))
Box::new(ok(None))
}
}

Expand Down
3 changes: 1 addition & 2 deletions crates/interledger-settlement-engines/src/stores/mod.rs
Expand Up @@ -13,11 +13,10 @@ pub type IdempotentEngineData = (StatusCode, Bytes, [u8; 32]);
pub trait IdempotentEngineStore {
/// Returns the API response that was saved when the idempotency key was used
/// Also returns a hash of the input data which resulted in the response
/// Returns error if the data was not found
fn load_idempotent_data(
&self,
idempotency_key: String,
) -> Box<dyn Future<Item = IdempotentEngineData, Error = ()> + Send>;
) -> Box<dyn Future<Item = Option<IdempotentEngineData>, Error = ()> + Send>;

/// Saves the data that was passed along with the api request for later
/// The store MUST also save a hash of the input, so that it errors out on requests
Expand Down
Expand Up @@ -43,7 +43,7 @@ impl IdempotentEngineStore for EngineRedisStore {
fn load_idempotent_data(
&self,
idempotency_key: String,
) -> Box<dyn Future<Item = IdempotentEngineData, Error = ()> + Send> {
) -> Box<dyn Future<Item = Option<IdempotentEngineData>, Error = ()> + Send> {
let idempotency_key_clone = idempotency_key.clone();
Box::new(
cmd("HGETALL")
Expand All @@ -65,13 +65,13 @@ impl IdempotentEngineStore for EngineRedisStore {
trace!("Loaded idempotency key {:?} - {:?}", idempotency_key, ret);
let mut input_hash: [u8; 32] = Default::default();
input_hash.copy_from_slice(input_hash_slice.as_ref());
Ok((
Ok(Some((
StatusCode::from_str(status_code).unwrap(),
Bytes::from(data.clone()),
input_hash,
))
)))
} else {
Err(())
Ok(None)
}
},
),
Expand Down Expand Up @@ -137,20 +137,23 @@ mod tests {
.load_idempotent_data(IDEMPOTENCY_KEY.clone())
.map_err(|err| eprintln!("Redis error: {:?}", err))
.and_then(move |data1| {
assert_eq!(data1, (StatusCode::OK, Bytes::from("TEST"), input_hash));
assert_eq!(
data1.unwrap(),
(StatusCode::OK, Bytes::from("TEST"), input_hash)
);
let _ = context;

store
.load_idempotent_data("asdf".to_string())
.map_err(|err| eprintln!("Redis error: {:?}", err))
.and_then(move |_data2| {
assert_eq!(_data2.0, StatusCode::OK);
.and_then(move |data2| {
assert!(data2.is_none());
let _ = context;
Ok(())
})
})
})
}))
.unwrap_err() // the second idempotent load fails
.unwrap()
}
}
Expand Up @@ -16,9 +16,7 @@ mod redis_helpers;
use redis_helpers::*;

mod test_helpers;
use test_helpers::{
create_account, get_balance, send_money, start_eth_engine, start_ganache, ETH_DECIMALS,
};
use test_helpers::{create_account, get_balance, send_money, start_eth_engine, start_ganache};

#[test]
/// In this test we have Alice and Bob who have peered with each other and run
Expand All @@ -29,6 +27,7 @@ use test_helpers::{
/// transactions, and once the transaction has sufficient confirmations it
/// lets Bob's connector know about it, so that it adjusts their credit.
fn eth_ledger_settlement() {
let eth_decimals = 9;
// Nodes 1 and 2 are peers, Node 2 is the parent of Node 3
let _ = env_logger::try_init();
let context = TestContext::new();
Expand Down Expand Up @@ -76,7 +75,7 @@ fn eth_ledger_settlement() {
.insert_account(AccountDetails {
ilp_address: Address::from_str("example.alice").unwrap(),
asset_code: "ETH".to_string(),
asset_scale: ETH_DECIMALS,
asset_scale: eth_decimals,
btp_incoming_token: None,
btp_uri: None,
http_endpoint: None,
Expand All @@ -93,13 +92,12 @@ fn eth_ledger_settlement() {
packets_per_minute_limit: None,
amount_per_minute_limit: None,
settlement_engine_url: None,
settlement_engine_asset_scale: None,
})
.and_then(move |_| {
node1_clone.insert_account(AccountDetails {
ilp_address: Address::from_str("example.bob").unwrap(),
asset_code: "ETH".to_string(),
asset_scale: ETH_DECIMALS,
asset_scale: eth_decimals,
btp_incoming_token: None,
btp_uri: None,
http_endpoint: Some(format!("http://localhost:{}/ilp", node2_http)),
Expand All @@ -119,7 +117,6 @@ fn eth_ledger_settlement() {
"http://localhost:{}",
node1_engine
)),
settlement_engine_asset_scale: Some(ETH_DECIMALS),
})
})
.and_then(move |_| node1.serve())
Expand All @@ -146,7 +143,7 @@ fn eth_ledger_settlement() {
.insert_account(AccountDetails {
ilp_address: Address::from_str("example.bob").unwrap(),
asset_code: "ETH".to_string(),
asset_scale: ETH_DECIMALS,
asset_scale: eth_decimals,
btp_incoming_token: None,
btp_uri: None,
http_endpoint: None,
Expand All @@ -163,14 +160,13 @@ fn eth_ledger_settlement() {
packets_per_minute_limit: None,
amount_per_minute_limit: None,
settlement_engine_url: None,
settlement_engine_asset_scale: None,
})
.and_then(move |_| {
node2
.insert_account(AccountDetails {
ilp_address: Address::from_str("example.alice").unwrap(),
asset_code: "ETH".to_string(),
asset_scale: ETH_DECIMALS,
asset_scale: eth_decimals,
btp_incoming_token: None,
btp_uri: None,
http_endpoint: Some(format!("http://localhost:{}/ilp", node1_http)),
Expand All @@ -190,7 +186,6 @@ fn eth_ledger_settlement() {
"http://localhost:{}",
node2_engine
)),
settlement_engine_asset_scale: Some(ETH_DECIMALS),
})
.and_then(move |_| node2.serve())
})
Expand Down Expand Up @@ -223,29 +218,29 @@ fn eth_ledger_settlement() {
.and_then(move |_| send1)
.and_then(move |_| {
get_balance(1, node1_http, "bob").and_then(move |ret| {
assert_eq!(ret, "{\"balance\":\"10\"}");
assert_eq!(ret, 10);
get_balance(1, node2_http, "alice").and_then(move |ret| {
assert_eq!(ret, "{\"balance\":\"-10\"}");
assert_eq!(ret, -10);
Ok(())
})
})
})
.and_then(move |_| send2)
.and_then(move |_| {
get_balance(1, node1_http, "bob").and_then(move |ret| {
assert_eq!(ret, "{\"balance\":\"30\"}");
assert_eq!(ret, 30);
get_balance(1, node2_http, "alice").and_then(move |ret| {
assert_eq!(ret, "{\"balance\":\"-30\"}");
assert_eq!(ret, -30);
Ok(())
})
})
})
.and_then(move |_| send3)
.and_then(move |_| {
get_balance(1, node1_http, "bob").and_then(move |ret| {
assert_eq!(ret, "{\"balance\":\"70\"}");
assert_eq!(ret, 70);
get_balance(1, node2_http, "alice").and_then(move |ret| {
assert_eq!(ret, "{\"balance\":\"-70\"}");
assert_eq!(ret, -70);
Ok(())
})
})
Expand All @@ -261,9 +256,9 @@ fn eth_ledger_settlement() {
// Since the credit connection reached -71, and the
// settle_to is -10, a 61 Wei transaction is made.
get_balance(1, node1_http, "bob").and_then(move |ret| {
assert_eq!(ret, "{\"balance\":\"10\"}");
assert_eq!(ret, 10);
get_balance(1, node2_http, "alice").and_then(move |ret| {
assert_eq!(ret, "{\"balance\":\"-10\"}");
assert_eq!(ret, -10);
ganache_pid.kill().unwrap();
Ok(())
})
Expand Down