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

Add optional boundaries to getregistrationreceipt #199

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

ShubhamBhut
Copy link

fixes #97

@mariocynicys @sr-gi can you pls have a look at this ? I am actually pretty new to rust.

Copy link
Member

@sr-gi sr-gi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First pass. Check comments inline.

Thanks for taking care of this, I love the proactivity.

Notice that the command does not work as intended at the moment if you pass more than one parameter to it. e.g:

lightning-cli getregistrationreceipt 03488e12bc580caaaa4f575bde2b4754746382fc5e8b5e3d03a632b0f15c5175cc 0 1
"Unexpected json format. Expected a single parameter. Received: 3"

This is due to the TowerId parsing I mentioned in one of the comments.

Notice, also, how you have only patched the test to cover the old case, but there are no tests for the new cases (hence why you didn't catch the errors on the updated function). This requires tests for both the old and the new cases, so we can make sure that both the latest receipt can be pulled if no boundary is specified, but also that every receipt within a boundary is returned.

Also, for the RPC specific changes, I'll suggest you manually try all the possible inputs (there are only two options, either start and end blocks are specified or not).

Comment on lines 6 to 11
pub const DEFAULT_SUBSCRIPTION_START: Option<i64> = None;
pub const SUBSCRIPTION_START: &str = "subscription-start";
pub const SUBSCRIPTION_START_DESC: &str = "subscription-start time";
pub const DEFAULT_SUBSCRIPTION_EXPIRY: Option<i64> = None;
pub const SUBSCRIPTION_EXPIRY: &str = "subscription-expiry";
pub const SUBSCRIPTION_EXPIRY_DESC: &str = "subscription-expiry time";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None of this is required. There is no such thing as a default subscription start or expiry. They depend on the block height at which the user requested the subscription.

Comment on lines 222 to 239
let mut query = "SELECT available_slots, subscription_start, subscription_expiry, signature FROM registration_receipts WHERE tower_id = ?1".to_string();

let mut params = vec![tower_id.to_vec()];

if let Some(start) = subscription_start {
query.push_str(" AND subscription_start >= ?2");
params.push(start.to_be_bytes().to_vec());
}

if let Some(expiry) = subscription_expiry {
query.push_str(" AND subscription_expiry <= ?3");
params.push(expiry.to_be_bytes().to_vec());
}

query.push_str(" ORDER BY subscription_expiry DESC LIMIT 1");

let mut stmt = self.connection.prepare(&query).unwrap();
let params: Vec<&dyn ToSql> = params.iter().map(|v| v as &dyn ToSql).collect();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notice that you've changed the behavior of this method.

If the bounds are not passed, the method should work exactly as it was before. Otherwise, it should have this new functionality.

Also, why are you limiting the result? This can potentially return multiple items.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, I will keep exactly old method in case of no optional parameters.
Actually I thought since old function was returning only latest receipt, new one should do same. I will remove the limit.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no need for two methods tho, just build the SQL query based on what the input params are.

Comment on lines 222 to 239
let mut query = "SELECT available_slots, subscription_start, subscription_expiry, signature FROM registration_receipts WHERE tower_id = ?1".to_string();

let mut params = vec![tower_id.to_vec()];

if let Some(start) = subscription_start {
query.push_str(" AND subscription_start >= ?2");
params.push(start.to_be_bytes().to_vec());
}

if let Some(expiry) = subscription_expiry {
query.push_str(" AND subscription_expiry <= ?3");
params.push(expiry.to_be_bytes().to_vec());
}

query.push_str(" ORDER BY subscription_expiry DESC LIMIT 1");

let mut stmt = self.connection.prepare(&query).unwrap();
let params: Vec<&dyn ToSql> = params.iter().map(|v| v as &dyn ToSql).collect();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check the params macro, I don't think we need to make it this complicated

Comment on lines 136 to 139
let tower_id = TowerId::try_from(v.clone()).map_err(|x| anyhow!(x))?;
let subscription_start = v["subscription-start"].as_u64().map(|v| v as u32);
let subscription_expiry = v["subscription-expiry"].as_u64().map(|v| v as u32);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not right. Given we are now potentially passing multiple params to the function, we cannot crate the TowerId as before, given v now can contain more data.

What is passed the the function is a json of the input params. Check in convert.rs how other commands handle this (you'll need to create a GetRegistrationReceiptParams struct and implement the parsing)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh my bad, will fix this.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have implemented RegistrationReceiptParams struct, but it seems when optional arguments are passed, the query fails, I haven't been able to figure out exact issue in code, code seems fine to me. Can you pls have a look

Copy link
Member

@sr-gi sr-gi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check comments inline.

You have still not provided tests where the boundaries are checked, only cases where both are None.

Regarding why this is not properly returning when you call it with the boundaries, the reason is that you are passing start and expiry as be_bytes, while they are defined in the database as numbers. You need to pas them as such.

@@ -3,7 +3,6 @@ pub const TOWERS_DATA_DIR: &str = "TOWERS_DATA_DIR";
pub const DEFAULT_TOWERS_DATA_DIR: &str = ".watchtower";

/// Collections of plugin option names, default values and descriptions

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line break is not needed

Comment on lines 202 to 203
"Unexpected request format. The request needs 2 parameter. Received: {param_count}"
)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, there's not actual change, this does not need reformating

watchtower-plugin/src/dbm.rs Outdated Show resolved Hide resolved
query.push_str(" AND subscription_expiry = (SELECT MAX(subscription_expiry) FROM registration_receipts WHERE tower_id = ?1)");
}

if let Some(expiry) = subscription_expiry {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both start and expiry need to be passed for the query to be valid. This is checking if they are independently passed (one OR the other, but not one AND the other).

You can either check this here and return an error, or in the main function and not call this method if the constraint is not met.

watchtower-plugin/src/convert.rs Show resolved Hide resolved
} else {
None
};
let subscription_expiry = if let Some(expire) = a.get(2).and_then(|v| v.as_i64()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned in the database method, this should only accept ether none or both sides of the boundary. I think this should be the right place to make that check, and fail to parse if both are not provided. That is, the parsing accepts either 1, or 3 params.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have made all the suggested changed, new function seems to be working perfectly in my local device.
Though I am getting output mismatch error for subscription_expiry in test_load_registration_receipt even though the function queries for exact same params used to store receipt in db.
error log:

thread 'dbm::tests::test_load_registration_receipt' panicked at 'assertion failed: `(left == right)`
  left: `RegistrationReceipt { user_id: UserId(PublicKey(7a7fd8cca7fd5d5e4584f7c6e1453ab67a7b834601c0bb04d9442dba9edee9f860947791f39d7033
d6a5de0e1b221472c60f82abc6585b959ddb399e17383844)), available_slots: 1414565319, subscription_start: 741713076, subscription_expiry: 7417
13496, signature: Some("dhrb3euw99g7gfrhf68ifwsnknro5ksbhs5zh4tfyapqbx5yb3d7rj4hih14fb8in6mmmgihng876pq1dy17t9jfcc1txaq7kycufdet") }`,
 right: `RegistrationReceipt { user_id: UserId(PublicKey(7a7fd8cca7fd5d5e4584f7c6e1453ab67a7b834601c0bb04d9442dba9edee9f860947791f39d7033
d6a5de0e1b221472c60f82abc6585b959ddb399e17383844)), available_slots: 1414565521, subscription_start: 741713076, subscription_expiry: 7417
13626, signature: Some("ryxxzerta61taatusksbcgpfkpdsaa7xb8ybsw9pwuath5nseqigndqp4ikhi6gemuaaaiza9ar7ufz8oi9p3k861xb1g69a76ndy6ky") }`', w
atchtower-plugin/src/dbm.rs:752:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

ShubhamBhut added a commit to ShubhamBhut/rust-teos that referenced this pull request Mar 16, 2023
Copy link
Member

@sr-gi sr-gi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks better now. There are still some things to have in mind though:

Notice that, given now we are working with a boundary, it could be the case that multiple registration receipts are a match for a certain boundary. That should be taken into account. This means that the dbm method will need to return a vector instead of a single item.

Also, the naming of the methods may need to be updated to make sense.

Regarding tests, you have updated them to cover the case where a boundary is passed, but now the case where it is not passed is not covered anymore. Think about what are all possible combinations of inputs and do test them all. Also test when multiple receipts are returned by the method

@@ -200,7 +200,7 @@ impl TryFrom<serde_json::Value> for GetAppointmentParams {
if param_count != 2 {
Err(GetAppointmentError::InvalidFormat(format!(
"Unexpected request format. The request needs 2 parameter. Received: {param_count}"
)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not needed.

Please do check unnecessary formatting changes before pushing.

Comment on lines 306 to 331
let subscription_start = if let Some(start) = a.get(1).and_then(|v| v.as_i64()) {
if start >= 0 {
Some(start as u32)
} else {
return Err(GetRegistrationReceiptError::InvalidFormat(
"Subscription-start must be a positive integer".to_owned(),
));
}
} else {
None
};
let subscription_expiry = if let Some(expire) = a.get(2).and_then(|v| v.as_i64()) {
if expire > subscription_start.unwrap() as i64 {
Some(expire as u32)
} else {
return Err(GetRegistrationReceiptError::InvalidFormat(
"Subscription-expire must be a positive integer and greater than subscription_start".to_owned(),
));
}
} else {
None
};
if subscription_start.is_some() != subscription_expiry.is_some() {
return Err(GetRegistrationReceiptError::InvalidFormat(
"Subscription-start and subscription-expiry must be provided together".to_owned(),
));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can merge all this into a single if let statement.

At this point you have already made sure that the provided number of params is either one or three, so there is no way the user is providing the subscription_start but not subscription_end.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, we are referencing subscription_start and subscription_expiry in snake case, but you are logging them as Subscription-{start, end}

params.push(v);
}
}
GetRegistrationReceiptParams::try_from(json!(params))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: add a line break here

GetRegistrationReceiptParams::try_from(json!(params))
}
},
_ => Err(GetRegistrationReceiptError::InvalidFormat(format!(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional parameters are usually expressed in square brackets, so this should read something like:

Unexpected request format. Expected: tower_id [subscription_start] [subscription_expire]. Received: '{value}'

match value {
serde_json::Value::Array(a) => {
let param_count = a.len();
if param_count != 1 && param_count != 3 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to emphasize the boundary not being fully passed (given this may be a common error), you can have a special case here for two params before checking one or three were you error that if a boundary is set, both ends of the boundary need to be specified. e.g:

if parame_count == 2 {...}
else if param_count != 1 && param_count != 3 {...}
else {...}

WHERE tower_id = ?1)",
)
.unwrap();
let mut query = "SELECT available_slots, subscription_start, subscription_expiry, signature FROM registration_receipts WHERE tower_id = ?1 AND (subscription_start >=?2 OR ?2 is NULL) AND (subscription_expiry <=?3 OR ?3 is NULL)".to_string();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels pretty hacky, especially when there is no boundary, well be passing (AND NULL is NULL) AND (NULL is NULL) which will be valid, but feels pretty unnecessary.

It'll be best to find a way to define the query and params list depending on whether subscription_start and subscription_expiry were passed

if let Some(response) = state.get_registration_receipt(tower_id) {
if let Some(response) =
state.get_registration_receipt(tower_id, subscription_start, subscription_expiry)
{
Ok(json!(response))
} else {
Err(anyhow!(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notice this is not the right error for every case now. It used to be the case because if no receipt could be pulled from the database it meant the tower was unknown. Now, if params have been passed, it could be the case that you could not find a receipt for the specified boundary. This needs to be covered. I'd suggest you check whether the tower can be found in memory if the query errors, and the return the proper error depending on that outcome.

ShubhamBhut added a commit to ShubhamBhut/rust-teos that referenced this pull request Mar 22, 2023
@ShubhamBhut
Copy link
Author

Hey @sr-gi,
Sincere apologies for the formatting mistakes. I believe this commit should solve everything; except I couldn't think of any new & better name for the updated methods.

Copy link
Member

@sr-gi sr-gi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this may be working now.

A few extra comments. I'll need to give a final pass to this once everything is covered.

All commits need to be squashed.

Err(GetRegistrationReceiptError::InvalidFormat(format!(
"Unexpected request format. The request needs 1 or 3 parameter. Received: {param_count}"
)))
} else{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A space is missing after {

watchtower-plugin/src/convert.rs Outdated Show resolved Hide resolved
watchtower-plugin/src/convert.rs Outdated Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why rust fmt is not properly formatting this, but here you have a proper formatted version:

diff --git a/watchtower-plugin/src/convert.rs b/watchtower-plugin/src/convert.rs
index d49889c..b9475de 100644
--- a/watchtower-plugin/src/convert.rs
+++ b/watchtower-plugin/src/convert.rs
@@ -289,20 +289,22 @@ impl TryFrom<serde_json::Value> for GetRegistrationReceiptParams {
         match value {
             serde_json::Value::Array(a) => {
                 let param_count = a.len();
-                if param_count == 2{
-                    Err(GetRegistrationReceiptError::InvalidFormat(("Both ends of boundary (subscription_start and subscription_expiry) are required.").to_string()))
+                if param_count == 2 {
+                    Err(GetRegistrationReceiptError::InvalidFormat(
+                        "Both ends of boundary (subscription_start and subscription_expiry) are required".to_string()
+                    ))
                 } else if param_count != 1 && param_count != 3 {
                     Err(GetRegistrationReceiptError::InvalidFormat(format!(
                         "Unexpected request format. The request needs 1 or 3 parameter. Received: {param_count}"
                     )))
-                } else{
+                } else {
                     let tower_id = if let Some(s) = a.get(0).unwrap().as_str() {
                         TowerId::from_str(s).map_err(|_| {
-                        GetRegistrationReceiptError::InvalidId("Invalid tower id".to_owned())
+                            GetRegistrationReceiptError::InvalidId("Invalid tower id".to_owned())
                         })
                     } else {
                         Err(GetRegistrationReceiptError::InvalidId(
-                        "tower_id must be a hex encoded string".to_owned(),
+                            "tower_id must be a hex encoded string".to_owned(),
                         ))
                     }?;

@@ -311,22 +313,24 @@ impl TryFrom<serde_json::Value> for GetRegistrationReceiptParams {
                             (Some(start as u32), Some(expire as u32))
                         } else {
                             return Err(GetRegistrationReceiptError::InvalidFormat(
-                                    "Subscription_start must be a positive integer and subscription_expire must be a positive integer greater than subscription_start".to_owned(),
-                                    ));
+                                "subscription_start must be a positive integer and subscription_expire must be a positive integer greater than subscription_start".to_owned(),
+                            ));
                         }
                     } else if a.get(1).is_some() || a.get(2).is_some() {
                         return Err(GetRegistrationReceiptError::InvalidFormat(
-                                "Subscription_start and subscription_expiry must be provided together as positive integers".to_owned(),
-                                ));
+                             "subscription_start and subscription_expiry must be provided together as positive integers".to_owned(),
+                        ));
                     } else {
                         (None, None)
                     };

-                    Ok(Self {
-                    tower_id,
-                    subscription_start,
-                    subscription_expiry,
-                    })
+                    Ok(
+                        Self {
+                            tower_id,
+                            subscription_start,
+                            subscription_expiry,
+                        }
+                    )
                 }
             },
             serde_json::Value::Object(mut m) => {

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for sharing the formatted code. Both rustfmt and cargo fmt are not formatting this code. Did it work for you or you had to format manually ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately I had to do it manually, I don't know why rust fmt seems not to properly handle big files with multiple levels of identation :(

"Subscription_start must be a positive integer and subscription_expire must be a positive integer greater than subscription_start".to_owned(),
));
}
} else if a.get(1).is_some() || a.get(2).is_some() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this branch is really necessary, is it?

We've already made sure that both the start and the end of the boundary have been provided, or none of them have. At this point is either the boundary is correct (start>=0 and end>start) or it is not, so it cannot be the case that either of them but not both are Some(x)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually the method was also allowing text(alphabetic letters) as subscription_start and subscription_expiry. I added this branch to handle this case. Should I remove this ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, right, that's because elements are passed as json Values and they can be anything really. I think this should be re-arranged then for clarity, since I didn't really get what was the point of having that else.

Can you make it so you capture the values in your if let as you are atm, but instead of convert them straight away you leave them as Values, and then you try to convert them to i64 or fail with a similar error to this one inside that same context?

e.g.:

let (subscription_start, subscription_expiry) = if let (Some(start), Some(expire)) = (a.get(1)), a.get(2)) {
    /// Try to convert them to i64, fail if they are not convertable
}

.unwrap();
subscription_start: Option<u32>,
subscription_expiry: Option<u32>,
) -> Vec<RegistrationReceipt> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should keep this an Option, so its either None or Some(Vec<...>)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thing is, after reworking this function to return a vector, it should never return None. It will return an empty vector if 1- the tower wasn't found & 2- the given range doesn't intersect with any subscription.

}
let mut stmt = self.connection.prepare(&query).unwrap();

let receipts = stmt
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need to collect this

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hey @sr-gi can you pls eloberate a little ? I am not seeing any collect here and I believe that below(at line 248) collect is required. Also I have changed that code to the code recommended by Omer now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By collecting I mean storing it in a temp variable. You're doing:

let receipts = stmt.query_map...
receipts

But you can simply return the statement call:

stmt.query_map...

))
})
.unwrap()
.collect::<Result<Vec<_>, _>>()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can simplify this to Result<_, _>. I'm not really sure why the annotation is needed tbh.

Any ideas @mariocynicys?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have written it like this:

let receipts = stmt
    .query_map(params.as_slice(), |row| {
        let slots: u32 = row.get(0)?;
        let start: u32 = row.get(1)?;
        let expiry: u32 = row.get(2)?;
        let signature: String = row.get(3)?;

        Ok(RegistrationReceipt::with_signature(
            user_id, slots, start, expiry, signature,
        ))
    })
    .unwrap() // MappedRows<|&Row| -> Result<RegistrationReceipt, E>>
    .map(|r| r.unwrap())
    .collect();

It's pretty interesting though how the type after the .unwrap() (MappedRows<|&Row| -> Result<RegistrationReceipt, E>> which is equivalent to a Vec<Result<RegistrationReceipt, E>> when collected) could be collected into Result<Vec<RegistrationReceipt>, E>.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can simplify this to Result<_, _>. I'm not really sure why the annotation is needed tbh.

I missed the point here 😂, yeah Result<_, _> is valid.

I would say .map(|res| res.unwrap()).collect(); feels more intuitive/less-hacky though.

@sr-gi
Copy link
Member

sr-gi commented Mar 23, 2023

There's something I just realized while manually testing this.

IIRC if two subscriptions overlapped, the user was only supposed to be storing the latest receipt. However, I just tested this and two receipts were returned where receipt_a ranged Ta-Tb, and receipt_b ranged Ta-Tc with c>b.

Does this ring a bell @mariocynicys?

No worries @ShubhamBhut, this is not due to your PR, but we may have forgotten to remove some data in an older PR.

@mariocynicys
Copy link
Collaborator

IIRC if two subscriptions overlapped, the user was only supposed to be storing the latest receipt. However, I just tested this and two receipts were returned where receipt_a ranged Ta-Tb, and receipt_b ranged Ta-Tc with c>b.

Revising this, we made the merger on the tower side and not the client side.
The client side though gets it's subscription summary in memory updated to the merger of the two overlapping receipts, but all the receipts are stored in the DB nonetheless.
We should delete those receipts since the last one already covers all of the past overlapping ones, so to get a cleaner output from getregistrationreceipt.

@sr-gi
Copy link
Member

sr-gi commented Mar 24, 2023

IIRC if two subscriptions overlapped, the user was only supposed to be storing the latest receipt. However, I just tested this and two receipts were returned where receipt_a ranged Ta-Tb, and receipt_b ranged Ta-Tc with c>b.

Revising this, we made the merger on the tower side and not the client side. The client side though gets it's subscription summary in memory updated to the merger of the two overlapping receipts, but all the receipts are stored in the DB nonetheless. We should delete those receipts since the last one already covers all of the past overlapping ones, so to get a cleaner output from getregistrationreceipt.

Referenced this here #208

@ShubhamBhut
Copy link
Author

I am having some issues with squashing, I will submit squashed PR after resolving them. I am submitting code for review in case my squashing issues remain longer.

ShubhamBhut added a commit to ShubhamBhut/rust-teos that referenced this pull request Mar 30, 2023
ShubhamBhut added a commit to ShubhamBhut/rust-teos that referenced this pull request Mar 30, 2023
@ShubhamBhut ShubhamBhut force-pushed the master branch 2 times, most recently from 6046848 to ed1be9e Compare March 30, 2023 10:23
@mariocynicys mariocynicys self-requested a review April 8, 2023 14:00
@ShubhamBhut ShubhamBhut requested a review from sr-gi April 9, 2023 09:27
Copy link
Member

@sr-gi sr-gi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some small changes. Looks good otherwise.

Remember to squash the commits.

Comment on lines 237 to 250
stmt.query_map(params.as_slice(), |row| {
let slots: u32 = row.get(0)?;
let start: u32 = row.get(1)?;
let expiry: u32 = row.get(2)?;
let signature: String = row.get(3)?;

Ok(RegistrationReceipt::with_signature(
user_id, slots, start, expiry, signature,
))
})
.unwrap()
.map(|r| r.unwrap())
.collect(),
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can simplify this:

Suggested change
stmt.query_map(params.as_slice(), |row| {
let slots: u32 = row.get(0)?;
let start: u32 = row.get(1)?;
let expiry: u32 = row.get(2)?;
let signature: String = row.get(3)?;
Ok(RegistrationReceipt::with_signature(
user_id, slots, start, expiry, signature,
))
})
.unwrap()
.map(|r| r.unwrap())
.collect(),
)
stmt.query_map(params.as_slice(), |row| {
let slots: u32 = row.get(0)?;
let start: u32 = row.get(1)?;
let expiry: u32 = row.get(2)?;
let signature: String = row.get(3)?;
Ok(RegistrationReceipt::with_signature(
user_id, slots, start, expiry, signature,
))
})
.unwrap()
.map(|r| r.ok())
.collect()

@@ -180,8 +180,18 @@ impl WTClient {
}

/// Gets the latest registration receipt of a given tower.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not true anymore

Err(anyhow!("No registration receipt found for {tower_id}"))
} else {
Err(anyhow!(
"Cannot find {tower_id} within the known towers. Have you registered ?"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove extra space before "?"

state.get_registration_receipt(tower_id, subscription_start, subscription_expiry);
if response.clone().unwrap().is_empty() {
if state.towers.contains_key(&tower_id) {
Err(anyhow!("No registration receipt found for {tower_id}"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given this implies that the tower is found, an it cannot be the case that a tower is found and a registration receipt is not, it means it can only be hit if a boundary is passed and there is no receipt for that boundary. Therefore:

Suggested change
Err(anyhow!("No registration receipt found for {tower_id}"))
Err(anyhow!("No registration receipt found for {tower_id} on the given range"))

Copy link
Member

@sr-gi sr-gi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK fc794fb

@sr-gi
Copy link
Member

sr-gi commented Apr 28, 2023

@mariocynicys care to give this a look when you have some time?

Copy link
Collaborator

@mariocynicys mariocynicys left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

Some nits and this is nearly finished, but there are some stuff I didn't grasp.

))
}?;

let (subscription_start, subscription_expiry) = if let (Some(start), Some(expire)) = (a.get(1), a.get(2)){
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Insert a space after (a.get(1), a.get(2)) before {.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Has this been fixed?

)
})?;

let expire = expire.as_i64().ok_or_else(|| {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can we name this variable expiry?

Comment on lines +312 to +316
let start = start.as_i64().ok_or_else(|| {
GetRegistrationReceiptError::InvalidFormat(
"Subscription_start must be a positive integer".to_owned(),
)
})?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want it a positive integer, shouldn't we use as_u64?

(Some(start as u32), Some(expire as u32))
} else {
return Err(GetRegistrationReceiptError::InvalidFormat(
"subscription_start must be a positive integer and subscription_expire must be a positive integer greater than subscription_start".to_owned(),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't they be the same value? If, for instance, I know what specific block I registered in.


if m.is_empty() || param_count > allowed_keys.len() {
Err(GetRegistrationReceiptError::InvalidFormat(format!("Unexpected request format. The request needs 1-3 parameters. Received: {param_count}")))
} else if !m.contains_key(allowed_keys[0]){
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing a space here as well, before the {.

let mut params: Vec<&dyn ToSql> = vec![&tower_id_encoded];

if subscription_expiry.is_none() {
query.push_str(" AND subscription_expiry = (SELECT MAX(subscription_expiry) FROM registration_receipts WHERE tower_id = ?1)")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add a ; at the end of this line.

} else {
query.push_str(" AND subscription_start>=?2 AND subscription_expiry <=?3");
params.push(&subscription_start);
params.push(&subscription_expiry)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and this one as well.

if subscription_expiry.is_none() {
query.push_str(" AND subscription_expiry = (SELECT MAX(subscription_expiry) FROM registration_receipts WHERE tower_id = ?1)")
} else {
query.push_str(" AND subscription_start>=?2 AND subscription_expiry <=?3");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please fix the spacing around the >= & <= (keep one space on each side).

Comment on lines +146 to +156
if response.clone().unwrap().is_empty() {
if state.towers.contains_key(&tower_id) {
Err(anyhow!(
"No registration receipt found for {tower_id} on the given range"
))
} else {
Err(anyhow!(
"Cannot find {tower_id} within the known towers. Have you registered?"
))
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really grasp this part. why is there an .unwrap()?!
response is an optional vector, so Option::None & an empty vector signal different meanings? right?

I guess Option::None is when the tower isn't found and an empty vector is when the registration range doesn't contain any receipts?
If so, then the .clone().unwrap() part will panic with no useful information to the user.

Otherwise, we don't need the None variant anymore.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that is actually what i think I was suggesting for, however, checking DBM::load_registration_receipt I don't think we ever produce a None return.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be good to test this out by creating a test that queries a tower that is not part of the DB and see whether the method is returning an empty vec or None

Comment on lines +246 to +248
.unwrap()
.map(|r| r.ok())
.collect()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't really get my head around how collect() produces Option<Vec< here 😕,
but aside from that, I see that the failure that's being accounted for in map(|r| r.ok()) is for the de-serialization of the data from the DB (slots, start, ...) which we assume shouldn't fail anyways.

I think we can unwrap while de-serializing directly since this shouldn't actually fail.
and substitute map(|r| r.ok()) with map(|r| r.unwrap()). This will make this method return a Vec< instead.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this was previous approach, but then sr-gi suggested to keep it Option<Vec<
ref

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think my point here was being able to distinguish between when no data was found because the tower was not even there, and when the range didn't produce any output. However, we are checking that afterward in main.rs by checking if the tower was part of the state. So this may indeed be simplified.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think my point here was being able to distinguish between when no data was found because the tower was not even there, and when the range didn't produce any output.

I would be for implementing this kind of logic, but the one implemented in here doesn't actually distinguish between the two cases. It always returns Some(a_possibly_zero_len_vec).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that is my point, that the suggestion was for that, but we really don't do that here, but always return Some(...) and check afterward.

I'm happy with either one or the other.

Comment on lines +262 to +275
#[derive(Debug)]
pub enum GetRegistrationReceiptError {
InvalidId(String),
InvalidFormat(String),
}

impl std::fmt::Display for GetRegistrationReceiptError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
GetRegistrationReceiptError::InvalidId(x) => write!(f, "{x}"),
GetRegistrationReceiptError::InvalidFormat(x) => write!(f, "{x}"),
}
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no distinction in usage between these two different error variants. I think we can combine them into one.
We might also be able to get rid of them and use an Err instead.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I looked how other methods had this implemented and followed them 😅. Which way do you think would be better, keeping as it is, merging into one error or removing this entirely ? I will make changes accordingly.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I noticed other param parsers are doing the same. I think we can make all of them to use Err(String)||Err(&'static str).

Which way do you think would be better, keeping as it is, merging into one error or removing this entirely ? I will make changes accordingly.

Try to remove it entirely, then we can do the same for other param errors in a follow up if it made sense.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Err seems to be working fine, just had to tweak the code where parsing was done. I think I would wait for decision on issue of Vec or Option<vec before final submission, rest changes are done.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason why this is here is to make it properly testable. If you look at the other params, there are test suits to make sure the proper error is returned.

I guess we should add them too for GetRegistrationReceipt now

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, got it. I will add tests for GetRegistrationReceipt params there. And in case of #199 (comment), I think it would be better as Some(Vec<>) just because its common way in rust-teos code.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And in case of #199 (comment), I think it would be better as Some(Vec<>) just because its common way in rust-teos code.

This would only make sense if #199 (comment) is carried out. I am fine with both ways, but the None variant has to carry a meaning and not just be there so the interface look similar to other methods.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add optional boundaries to getregistrationreceipt
3 participants