-
Notifications
You must be signed in to change notification settings - Fork 3.6k
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
[move] Loader V2 API integration for MoveVM #14075
Conversation
⏱️ 1h 59m total CI duration on this PR
|
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.
does it make sense to add script_hash to CompiledScript or Script? it could also be smt we build e.g. in constructors.
|
||
let (data, bytes_loaded) = match loader { | ||
Loader::V1(_) => { | ||
let maybe_module = module_store.module_at(&ty_tag.module_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.
matter of style, but as a fancy fan got to mention we can directly compute the metadata by chaining .map_or_else(|| &[], |m|.module().metadata());
on one hand maybe_module isn't used elsewhere, on the other hand it makes reading a bit clearer. Similarly match might be more verbose - so up to you.
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 don't think we can use a map because then the lifetimes become weird... let's see if I can refactor the code so that it works, mapping or else is nicer indeed.
// If we need to process aggregator lifting, we pass type layout to remote. | ||
// Remote, in turn ensures that all aggregator values are lifted if the resolved | ||
// resource comes from storage. | ||
self.remote.get_resource_bytes_with_metadata_and_layout( |
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.
looks like we can move these calls in V1 and V2 outside? we just need to get different metadata?
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.
Hm, not really, we have different lifetimes here because the V2 returns a reference which lives as long as module storage lives, and V1 lives only as long as the optional module lives. And it gets this ugly. WIll try to refactor the code a bit to make it look nicer
PartialVMError::new(StatusCode::FUNCTION_RESOLUTION_FAILURE) | ||
.with_message(format!("Module {} doesn't exist", module_name)) | ||
})?; | ||
// Note(George): V2 loads function directly to avoid this case completely! |
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.
is whatever the below was precautioning from no longer relevant? then maybe we can add one sentence to sketch that.
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.
Added a sentence, but yes: V1 uses cache which maybe empty, but then there is this assumption that module must be in module cache when we load a function. This seems pretty fragile, and in V2 we always fetch a module from module storage and cache on read.
@@ -645,6 +667,15 @@ impl Interpreter { | |||
.map_err(|err| err.to_partial()) | |||
}, | |||
NativeResult::LoadModule { module_name } => { | |||
if let Loader::V2(_) = resolver.loader() { |
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.
mhm, why do we need a new check?
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.
Below we do check_dependencies_and_charge_gas
. Current implementation uses annoying allow_module_failure
only for the first set of ids
because these dependencies indeed may not exist (e.g., non existent entry function, etc.) but for other dependencies down the closure they always exist. So we optionally check if allow LINKER_ERROR or it is an invariant violation
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.
But actually yes, in this implementation I do not think we need this here, good point
1b082d8
to
65eb531
Compare
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.
Thanks a lot for this George, the fact that you keep improving everything around your changes is amazing
@@ -171,7 +172,7 @@ impl<'r> TransactionDataCache<'r> { | |||
Ok(change_set) | |||
} | |||
|
|||
pub(crate) fn num_mutated_accounts(&self, sender: &AccountAddress) -> u64 { | |||
pub(crate) fn num_mutated_resources(&self, sender: &AccountAddress) -> u64 { |
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.
is it resources? so it matches the account and counts mutated entries associated with the account, suppose that makes sense and thanks for fixing! however,
- let's also change inner variable names?
- total_mutated accounts is initialized w. 1 because there is an account resource? but that seems ugly: what if we call it with some account that wasn't even relevant though, it would just return 1? 'sender' name maybe helps a bit 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.
Yes, because it uses data_map
in its implementation and not modules. This nicely aligns with the fact that we will not store modules inside anymore👍
To be honest, I am not sure how useful this API is, it is only used for tests, but I know that not everyone is happy with removing public interfaces no matter what😄 But this seems so ugly... I agree, why we assume sender is mutated? Mu guess is that this must be the adapter's responsibility, where we have senders.
Example of a test:
// The resource was published to "account1" and the sender's account
// (TEST_ADDR) is assumed to be mutated as well (e.g., in a subsequent
// transaction epilogue).
assert_eq!(sess.num_mutated_resources(&TEST_ADDR), 2);
But this is just wrong, test assumes that something is mutated....
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.
So I see two ways:
- Remove this assumption on sender. Move doesn't care about this and epilogue, this is clearly off.
- Remove this API completely, or better make it
#[cfg(feature = "testing")]
or something like that.
@gelash what do you think?
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.
let's see if we are missing something, o.w. I'd purge 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.
cc @runtian-zhou @vgao1996 who might have some context about this
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.
Ping @runtian-zhou @vgao1996
.map_err(|err| self.attach_state_if_invariant_violation(err, ¤t_frame))?; | ||
// Select which resolver to use: if we are executing in the context of the entrypoint | ||
// function, we use the one provided by the caller. | ||
let resolver = if self.call_stack.is_empty() { |
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.
is this new logic? previously it worked through Script / Module distinction, right? let's be careful here, but it does seem we were creating resolvers every time for scripts and now we re-use? can call stack be cleared e.g. on abort, and then have unintended consequences? just asking.
Maybe we can encapsulate the logic somewhere (and add any other safeguards/checks if needed)?
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.
Yes, good point. I rewrote that ugly APIs by storing LoadedFunction
everywhere, and providing it with scope. @runtian-zhou I think this looks cleaner, and V2 loader API fit perfectly. WDYT? See the last commit in this PR (Note: I did not polish certain types, etc., but added this code to give an idea).
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.
Update: this change is refactored into #14185
) -> PartialVMResult<Self> { | ||
let module_id = function | ||
.module_id() | ||
.ok_or_else(|| { |
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 could move this inside module_id call, with error saying "Function {} must have a module owner", i.e. not always, but when we call, it should.
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.
Hmmm, yes, but scripts still do not have it is not really an error, there a few debug use cases where we optionally print things. My take is that the function should not be owned, but LoadedFunction
should be because this is the one that has full context, and can actually carry arced module or script. @runtian-zhou, what do you think?
I have tried that approach, and it requires changing types from Arc to LoadedFunction throughout, and I was not convinced this is better than just special casing for scripts.
f80b102
to
4b3f255
Compare
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.
My general take is we can don't need to version the code just for the sake of switching things over. The new loader should be functionally equivalent so if keeping two versions at the same time is making the code harder to read and maintain we don't have to keep both logics. WDYT?
@@ -60,6 +62,7 @@ impl<'r, 'l> Session<'r, 'l> { | |||
args: Vec<impl Borrow<[u8]>>, | |||
gas_meter: &mut impl GasMeter, | |||
traversal_context: &mut TraversalContext, | |||
module_storage: &impl ModuleStorage, |
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.
Would it make more sense to move the module_storage into Session? That way we could avoid changing the api. Also from the api standpoint I think it also makes sense, as we shouldn't expect modules loaded within a session to be altered.
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.
Not really, and this is something I do not want to do on purpose - mostly because of init-module and respawned sessions. If we capture it in Move session, then how do you support init_module? It is not possible unless you respawn session or stash published modules temporarily inside the session (which exactly lead to existing problems).
The APIs can made nice by introducing a "context" struct which you pass everywhere, but it contains the gas, the module storage, etc, something similar to Resolver
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.
Personally I would be hesitant to just land the PR as is because we are using dummy storage everywhere, meaning this V2 logic isn't actually being used and tested for now right? Or did I miss anything?
7a9cbfa
to
0c56eff
Compare
Description
ScriptStorage
andModuleStorage
are passed as references to loader APIs. This allows us to cache things on read.New data structures:
V: Verifier
type param to make sure we can provide custom verifier when loading a modulefetch_or_create_verified_module
- this function actually loads the transitive closure of dependencies, and this is an implementation detail of code cache (not here).fetch_or_create_verified_module
that is justf: &dyn Fn(Arc<CompiledModule>) -> PartialVMResult<Module>
and is passed by the loader. This way this function can capture struct name cache which maps struct names to identifiers, native functions, etc.Alternative design
We can have a shared context which will be shared between loader & code cache: something like this:
Type of Change
Which Components or Systems Does This Change Impact?
How Has This Been Tested?
Key Areas to Review
Checklist