-
Notifications
You must be signed in to change notification settings - Fork 11.2k
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 gas logic for move call and module publish #119
Conversation
Good find! FWIW I think the "all mutable inputs must be written" is too heavy-handed even in the success case. For example, one can write legitimate Move code like the following that violates the rule:
|
I might misunderstand something, but I think we will not need this. The Move VM lets you use a custom "gas schedule" that specifies the cost for each instruction. If we set these costs to whatever is appropriate for FastX, I think that should be all we need? |
Do we want to do that though? It seems convenient to just reuse Move's metering, given that we use a strictly subset of their bytecode, with identical semantics. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For ensuring the code paths we write all make sense over time, would it be safer to make all calls with a super high gas budget MAX_GAS_BUDGETS, instead of passing None
where a gas budget should be?
All comments addressed:
|
fastx_types/src/gas.rs
Outdated
Ok(()) | ||
} | ||
OrderKind::Publish(publish) => { | ||
assert_eq!(publish.gas_payment.0, gas_object.id()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should probably use debug_assert
's here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Curious when should we use debug_assert vs assert? From C++ world we use debug assert for things that are expensive to assert. In this case it seems pretty cheap.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question for @huitseeker. I think what we've been doing elsewhere in fastnft is using debug_assert
for any internal invariant violation (for both efficiency and liveness reasons). But of course whether or not to leave asserts (even cheap ones) on in prod is a deeply philosophical issue...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm probably not telling you anything new, but debug_assert
statements are elided in non-debug builds, so their cost is precisely zero.
As to assert
statements, being macros, they're programmatically easy to track, intercept and instrument after the fact, so that keeping their performance cost in check does not seem to be a big issue.
What I am worried by, however, is that they create panic code paths, which in production environments where a lot of inputs can be controlled by the adversary, and where cascading node failures lead to total system failure by definition, often prompt developers to reach for expletives.
So for each of those asserts I'd like to ask: is there any way at all that this panic could be recovered from?
I have a longer spiel on error management, I'll be happy to devote a doc / crypto meetup to it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that no input should be able to trigger an assertion (errors should be used in that case).
And no (proper use of) assertions should be recoverable.
Assertions should be used only in cases that indicate our implementation has a bug that would lead to inconsistent data down the road if continuing. Our production code will have bugs. So ideally I would love to be able to see the stack trace of the assertions in prod and be aware of it when it actually get hit, instead of debugging inconsistent data store in the system if we let the code to continue.
So my preference is:
- For anything that could happen given certain input, we use errors
- For anything that our implementation guarantees won't happen (e.g. a few lines back something is already checked, and latter on we assert on it), if it's really cheap (like a single equality check) we use assert, otherwise we use debug_assert.
Curious what you think.
Ah, I think we're on the same page? I am indeed suggesting that we reuse the metering (which is cost-agnostic), but provide custom instruction costs (which the VM supports via taking a parameter of type Totally ok with using the default
|
Cool! Looks like we only need to customize the |
Removed the conversion between move gas and fastx gas. |
Hey, all mutable objects need to at least have their sequence number incremented and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice one -- a few comments on TODOs for follow-up issues.
@@ -112,6 +114,11 @@ impl AuthorityState { | |||
FastPayError::IncorrectSigner | |||
); | |||
|
|||
if idx + 1 == input_object_cnt { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May be it would be slightly more clear if we add a get_gas_object_ref
to the order, and then we compare this ref to the object_ref
supplied. We will simply forget that the last object is special.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The orders are actually already structured this way, so I think all we should need to do is refactor the code above for resolving input ObjectRef
's to objects a bit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The orders are actually already structured this way, so I think all we should need to do is refactor the code above for resolving input
ObjectRef
's to objects a bit.
Not sure what you mean, but the Transfer order currently don't have a gas object yet. (in fact, this "last object" check works for transfer order today only because all of our objects are gas objects, so it's incomplete hack.) So everything will work properly only after I add gas support for Transfer order.
} | ||
|
||
pub fn calculate_module_publish_gas(module_bytes: &[Vec<u8>]) -> u64 { | ||
// TODO: Figure out module publish gas formula. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we parametrize this by the global table of Gas cost, with something like Gas_per_byte * bytes_stored. We probably also need to charge for gas for non module objects stored for Calls, and maybe reclaim some gas when objects are deleted?
Addressed the following comments (pushed as a second commit on top of the first one):
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just nits! lgtm
fastx_types/src/object.rs
Outdated
pub fn as_move(&self) -> Option<&MoveObject> { | ||
use Data::*; | ||
match self { | ||
Move(m) => Some(m), | ||
Module(_) => None, | ||
} | ||
} | ||
|
||
pub fn as_move_mut(&mut self) -> Option<&mut MoveObject> { | ||
use Data::*; | ||
match self { | ||
Move(m) => Some(m), | ||
Module(_) => None, | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: This may confuse folks used to AsRef<T>
and AsMut<T>
implementations. Inserting a try_
somehwere in the naming pattern might help.
macro_rules! ok_or_gas_error { | ||
($cond:expr, $e:expr) => { | ||
if !($cond) { | ||
Err(FastPayError::InsufficientGas { error: $e }) | ||
} else { | ||
Ok(()) | ||
} | ||
}; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: I would have left the boolean-to-Result
logic, which people are used to, and just macro-ed the error, but YMMV.
@@ -194,13 +199,23 @@ fn make_dependent_module(m: &CompiledModule) -> CompiledModule { | |||
dependent_module | |||
} | |||
|
|||
fn check_gas_object( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[cfg(test)]
would be good to make sure we don't trigger panics through an errant call to this.
This commit introduces a new command to fetch multiple blocks at once.
This commit introduces a new command to fetch multiple blocks at once.
This PR adds gas deduction logic for move call orders and module publish orders. I will add gas logic for transfer orders separately. The gas cost model is arbitrary at the moment. We will need to come up with a proper gas model latter.
Here is how it works:
handle_order
), we check that the gas object exists and meets the minimum requirements: has enough balance to cover budget in the case of move call, and meets minimum balance requirement.