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

[fastx dist sys] Connect vm adapter to handle-confirmation logic #62

Merged
merged 10 commits into from
Dec 17, 2021

Conversation

gdanezis
Copy link
Collaborator

@gdanezis gdanezis commented Dec 15, 2021

This PR finishes issue #8 to support transactions with multiple inputs and outputs, and a connection to the real vm adapter.
Specifically:

  • We make the object store an immutable ref, and use interior mutability (Arc/Mutex) to allow concurrent access and multiple owners.
  • We create a local execution context / store 'AuthorityTemporaryStore' to provide objects to the execution, and collect result. We then process these results in 'handle_confirmation_order' to create new locks and objects.
  • Update the adapter to use correct sequence number (issue [fastx adapter] new Move VM API that accepts objects as input #30 )

type Error = FastPayError;

fn get_resource(
&self,
address: &AccountAddress,
struct_tag: &StructTag,
) -> Result<Option<Vec<u8>>, Self::Error> {
match self.objects.get(address) {
match self.authority_state.objects.lock().unwrap().get(address) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Help! @sblackshear

Am I correct to think here either (1) a resource is listed in the input objects or (2) the resource is read-only. If this is the case, I will modify the read here to read from the 'AuthorityTemporaryStore' objects first, and then the global store (but check the read_only).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Same for the module lookup, but modules are always read-only so there this is invariant is trivial.

Copy link
Collaborator

@sblackshear sblackshear Dec 15, 2021

Choose a reason for hiding this comment

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

Good question. Actually, get_resource is only called when the Move VM touches Move global state, which we don't allow in fastX (https://github.com/MystenLabs/fastnft/blob/main/fastx_programmability/verifier/src/global_storage_access_verifier.rs is the mechanism for preventing this). So I think raising an Err corresponding to an internal invariant violation is probably the thing to do for now?

In the future, if we allow dynamic accesses of read-only objects, I think we would want to support it exactly as you suggest.

@gdanezis gdanezis marked this pull request as draft December 15, 2021 11:29
Copy link
Collaborator

@sblackshear sblackshear left a comment

Choose a reason for hiding this comment

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

Excited for this!

fastpay_core/src/authority.rs Outdated Show resolved Hide resolved
fastpay_core/src/authority.rs Show resolved Hide resolved
fastpay_core/src/authority.rs Outdated Show resolved Hide resolved
fastpay_core/src/authority.rs Outdated Show resolved Hide resolved
fastpay_core/src/authority.rs Outdated Show resolved Hide resolved
}

// If it exists remove it
if let Some(removed) = self.objects.remove(id) {
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 raising an internal invariant violation if we try to remove something nonexistent would make sense

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Right now I panic if it does not exist.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Where is the panic? I think this will silently fall through to 467 if id does not exist in objects (which is the case I'm concerned about).

@gdanezis gdanezis marked this pull request as ready for review December 15, 2021 15:43
@gdanezis gdanezis requested a review from sblackshear December 15, 2021 15:44
@gdanezis gdanezis self-assigned this Dec 15, 2021
fastpay_core/src/authority.rs Outdated Show resolved Hide resolved
}

// If it exists remove it
if let Some(removed) = self.objects.remove(id) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Where is the panic? I think this will silently fall through to 467 if id does not exist in objects (which is the case I'm concerned about).

fastpay_core/src/authority.rs Outdated Show resolved Hide resolved
// mutate objects if they are &mut so they cannot be read-only.
panic!("Internal invariant violation: Mutating a read-only object.")
}
self.written.push(object.to_object_reference());
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 checking no duplicates (either here or at into_inner) is a useful sanity check.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I will stick this behind a test conditional config.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done: check for duplicate write, duplicate delete, duplicates across write & delete, and the number of writes+delete is equal to the mutable inputs.

@@ -460,26 +481,102 @@ impl AuthorityState {
}
}

impl Storage for AuthorityState {
pub struct AuthorityTemporaryStore<'a> {
pub authority_state: &'a AuthorityState,
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 be in favor of removing the pub from each of these fields + exposing getters for any that we want to allow folks to read. There are invariants about the relationship between fields like objects and deleted that are enforced by the functions exposed, but could be violated by someone accessing these fields directly.

Copy link
Contributor

@huitseeker huitseeker Dec 15, 2021

Choose a reason for hiding this comment

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

Please have a look at #[readonly::make]: https://github.com/dtolnay/readonly

George: None of them have to be pub, so all good.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Totally, done. When I build a struct i start with pub members, otherwise the type system complains they are not used, and as a result I forget them as pub. My bad.

Copy link
Contributor

@huitseeker huitseeker left a comment

Choose a reason for hiding this comment

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

One main Q on read-only violations as a non-recoverable error.

fastpay_core/src/authority.rs Show resolved Hide resolved
@@ -460,26 +481,102 @@ impl AuthorityState {
}
}

impl Storage for AuthorityState {
pub struct AuthorityTemporaryStore<'a> {
pub authority_state: &'a AuthorityState,
Copy link
Contributor

@huitseeker huitseeker Dec 15, 2021

Choose a reason for hiding this comment

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

Please have a look at #[readonly::make]: https://github.com/dtolnay/readonly

George: None of them have to be pub, so all good.

");
Ok(Some(m.contents.clone()))
}
other => unimplemented!(
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it possible that a #[non_exhaustive] attribute is missing somewhere in Data?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Here @sblackshear may have a view, as he designed the 'Data' abstraction?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Did not know about this feature, very cool! Yes, would make sense to add this on Data

Comment on lines 544 to 626
if object.is_read_only() {
// This is an internal invariant violation. Move only allows us to
// mutate objects if they are &mut so they cannot be read-only.
panic!("Internal invariant violation: Mutating a read-only object.")
}
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the value in making this (and other places) an irrecoverable panic versus returning a Result, or some other indication of success / failure of the operation? It seems that writing to a read-only object might be a common mistake?

Copy link
Collaborator Author

@gdanezis gdanezis Dec 16, 2021

Choose a reason for hiding this comment

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

Right now we are restricted in terms of interface to what the movevm expects from a Storage I believe, so I have to follow the signatures that return no Result. At the same time I would rather check some of these invariants.

Note these are internal invariants, if the move adaptor is implemented correctly they should never trigger. Let me stick them behind a test comp flag to only use when refactoring, but not in prod -- another reason to not change the signatures.

Copy link
Contributor

Choose a reason for hiding this comment

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

This concern for an object being or not being read-only is everywhere in the code at this stage. I'd be comfortable with a debug_assert, which tells me this should never fire outside egregious misuse, or asking the move VM to change its APIs, whichever suits you best.

Copy link
Collaborator

@sblackshear sblackshear Dec 16, 2021

Choose a reason for hiding this comment

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

  • Although this is an internal invariant violation, I think should eventually be a Result so we can propagate it up to someone that can log it.
  • This code is inside Storage::write_object(&self, Object), which is a FastNFT trait under our control. I think that API should indeed be changed to use Result everywhere, but perhaps in a separate PR. This won't cause any problems for Move--it will just need to handle the Result inside the adapter.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah ok, I was confused about the provenance of Storage. Lets indeed make it fire results, but I also think we should not be spending runtime resources checking for errors in our code, again and again.

fn delete_object(&mut self, id: &ObjectID) {
self.objects.remove(id);
// Check it is not read-only
#[cfg(test)] // Movevm should ensure this
Copy link
Collaborator

Choose a reason for hiding this comment

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

@huitseeker: thoughts about cfg(test) vs debug_assert vs other options for assertions we want to fire everywhere except in release builds?

Copy link
Contributor

@huitseeker huitseeker Dec 16, 2021

Choose a reason for hiding this comment

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

assertions we want to fire everywhere except in release builds?

Not #[cfg(test)], that only fires in testing.
debug_assert is made for this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ok, thanks @sblackshear & @huitseeker for all the feedback:

  • I have now moved the checks to a separate function.
  • I have used the HashSet pattern to keep code short.
  • I have improved the check to ensure all mutable objects are either written or deleted.
  • As a result I caught a bug (we were not updating gas object in module upload) and fixed it.

Copy link
Contributor

@huitseeker huitseeker left a comment

Choose a reason for hiding this comment

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

Hopefully I've left comments that can improve at least the quadratic duplicate checks.

Comment on lines 539 to 544
for (i, write_ref) in self.written.iter().enumerate() {
for (j, write_ref2) in self.written.iter().enumerate() {
if i != j && write_ref == write_ref2 {
panic!("Invariant violation: duplicate writing.");
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This is :

debug_assert!({
  let mut used = HashSet::new();
  self.written.all(move |elt| used.insert(elt))
})

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed, see above

Comment on lines 553 to 559
for (k, del_ref) in self.deleted.iter().enumerate() {
for (l, del_ref2) in self.deleted.iter().enumerate() {
if k != l && del_ref == del_ref2 {
panic!("Invariant violation: duplicate deletion.");
}
}
}
Copy link
Contributor

@huitseeker huitseeker Dec 16, 2021

Choose a reason for hiding this comment

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

See above for refactoring a uniqueness check to linear time with a HashSet.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed, see above

Comment on lines 546 to 551
for del_ref in &self.deleted {
if write_ref == del_ref {
panic!("Invariant violation: both writing and deleting same object.");
}
}
}
Copy link
Contributor

@huitseeker huitseeker Dec 16, 2021

Choose a reason for hiding this comment

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

You could just have a list of distinct object refs and a bitmap or compressed bitmap for the deleted ones, with the rest being written.

But if you happen to like this two-vectors approach, ideally, you'd be able to reuse the HashSets you're building above and below to make sure that before inserting something in the second HashSet, you're checking for non-presence in the prior one.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I kept the two vector approach but refactored the test using HashSets. Fixed, see above.

Comment on lines 537 to 538
// Check a number of invariants. A correct vm calling the struct should ensure these
// but to help with testing and refactoring we check again here.
Copy link
Contributor

Choose a reason for hiding this comment

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

Please export this invariant-checking to a function.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yep, now in a separate function. Fixed, see above

Vec<ObjectRef>,
Vec<ObjectRef>,
) {
#[cfg(test)]
Copy link
Contributor

Choose a reason for hiding this comment

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

This only fires in testing, I suspect you mean debug_assert.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for this -- now all behind debug_assert

@gdanezis
Copy link
Collaborator Author

Hey @huitseeker - do a check and then we can land this?

Copy link
Contributor

@huitseeker huitseeker left a comment

Choose a reason for hiding this comment

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

Nit: Ah, ok! I see better why you were looking at a config flag now.

The PR is OK in its current state, but here's what I think you were trying to do: using #[cfg(debug_assertions)]

You could make your checking function check_invariants:

  • not use debug_assert itself,
  • but return a boolean and an optional message instead
  • then in the main function you would:
#[cfg(debug_assertions)]
{
    let (ok, msg) = check_invariants();
    debug_assert!(ok, "{}", msg);
}

The advantage being that absolutely nothing runs if you're not in debug mode, not even the function call.

@gdanezis
Copy link
Collaborator Author

Nit: Ah, ok! I see better why you were looking at a config flag now.

The PR is OK in its current state, but here's what I think you were trying to do: using #[cfg(debug_assertions)]

You could make your checking function check_invariants:

  • not use debug_assert itself,
  • but return a boolean and an optional message instead
  • then in the main function you would:
#[cfg(debug_assertions)]
{
    let (ok, msg) = check_invariants();
    debug_assert!(ok, "{}", msg);
}

The advantage being that absolutely nothing runs if you're not in debug mode, not even the function call.

Nice one. I used a simpler variant of the above to avoid the call. (I suspect the compiler already avoided the call, but to be safe).

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.

3 participants