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

Finish implementing initial Simulation interface #26

Closed
wants to merge 17 commits into from

Conversation

gjtrowbridge
Copy link
Contributor

@gjtrowbridge gjtrowbridge commented Feb 11, 2020

This commit implements all of the methods from the initial Simulation interface.

Notes:

  • Some logic is new, some logic was ported from notion-server/src/ethereum/mod.rs. In a future diff, notion-server/src/ethereum/mod.rs will be removed entirely, and the server logic in notion-server will instead point to eth2/simulation/.
  • Some reasons for this move / refactor:
    • Libraries used in implementing eth2-specific types were not compatible with libraries used by notion-server (eg. Rocket).
    • It didn't make sense to have the simulation and the server wrapping it be tightly coupled long-term, this approach is much more modular, and eg. will support using the simulation in other contexts also (eg. other wrappers could use the underlying Simulation logic, devs could use the Simulation logic directly without going through a wrapper, etc).

Next Steps:

  1. Update notion-server to use the Simulation logic in the eth2/simulation repository.
  2. Remove Simulation logic from notion-server.
  3. Verify that notion-server is working and satisfies all current user needs.
  4. Write an SDK library that can be imported into 3rd party projects and which abstracts out communication with notion-server. This will allow other projects to easily communicate with notion-server without needing to "manually" write HTTP requests.
  5. Continue updating the notion-server, SDK, and Simulation interfaces as-necessary as user needs change.

a: args::CreateExecutionEnvironment,
) -> Result<EEIndex> {
// Create EE struct from args
let vl_wasm_code =
Copy link
Contributor

Choose a reason for hiding this comment

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

Is the vl_ prefix a convention from the lighthouse repository?

I think I'd prefer to see just wasm_code, and use the shorthand assignment in the ExecutionEnvironment initialization.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree, I'll just use wasm_code.

#[derive(Debug, Snafu)]
pub enum Error {
#[snafu(display("{} exceeds max allowable length", what))]
MaxLengthExceeded { what: String },
Copy link
Contributor

Choose a reason for hiding this comment

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

Is MaxLengthExceeded always triggered from a ssz_types::Error?

Either way, I'd add a source field here, to give more context on the error.

Copy link
Contributor Author

@gjtrowbridge gjtrowbridge Feb 12, 2020

Choose a reason for hiding this comment

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

Correct, currently MaxLengthExceeded is only triggered from a ssz_types::Error. However, I could envision it being useful for other types of errors in the future.

Though, unless I'm misuderstanding Snafu, I believe that 1) I can only use context()?; if I add a source field (unless I'm calling .context() on a function returning an Option), and 2) this error type will ONLY ever be able to be used for ssz_types::Error if I do that approach, which could be OK but if so I'd want to change the name from MaxLengthExceeded to something like SszTypeError or similar.

One last note: my original implementation did use Snafu's canonical .context(...)?; pattern, along with a source field. However, ssz_types::Error doesn't actually implement std::error::Error, which means that ssz_types::Error can't be used as a source error. Obviously, I can update the ssz_types::Error type to implement std:error::Error, but I opted not to do this immediately, since I anticipate that ssz_types will eventually be a crate instead of "owned code" in this repo. That said, prior to pushing this as a crate, I'm sure the ssz_types authors would make the Error type compliant with the std::error::Error trait.

TL;DR: I don't feel too strongly here, as I see it, there are 2 paths forward:

  1. Leave it as-is, and update in the future if we want the granularity / lower-level visibility into the ssz_types error.
  2. Change to have this use a source field now, which would entail:
  • Updating the ssz_types::Error field to implement the std:error:Error trait
  • Likely changing the name of this wrapper error from MaxLengthExceeded to something like SszTypeError, and have the display implementation of the wrapper just show the underlying display implementation of the ssz error (which I would have just added in the first bullet point). This way the error could wrap any of the ssz error types that may come back.

Either way works, and #2 is fairly easy to do. #2 has the advantage of the lower-level visibility, and would make it easier to give more info like what the max length was when it was exceeded. #1 is obviously already in use, and has the advantage of in my opinion being simpler and more readable.

My preference is #1 for now unless/until we have a compelling need for #2, but if you or others feel strongly #2 is easy enough to implement.

) -> Result<EEIndex> {
// Create EE struct from args
let vl_wasm_code =
VariableList::new(a.wasm_code).map_err(|_| Error::MaxLengthExceeded {
Copy link
Contributor

Choose a reason for hiding this comment

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

You should be able to use snafu's ResultExt::context method for these types of conversions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same comment as above -- interested to hear your thoughts on my breakdown of the two options. As mentioned, I have a slight preference for this because it is simpler, uses std library methods, and IMO is more readable.

})?;

// For each shard, add the initial state to the shard
for shard_state in self.store.current_beacon_state.shard_states.iter_mut() {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think I'd like to see a little more encapsulation here. Instead of reaching through multiple levels of struct's internal fields, create some helper methods. Should reduce the amount of duplicate code (especially error conversion.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the comment! I don't think I agree for this particular case, though it's possible I'm misunderstanding your point here. What kind of helper function do you envision?

My thoughts:

  • I don't expect any code duplication from this sort of thing -- chaining properties like this seems normal and readable, and current_beacon_state will always have shard_states, etc.
  • Not sure what you mean by error conversion -- there's no error conversion here. Let me know what I'm missing.

I could see this being applicable in other contexts, is there another line or example that you think could benefit from helper methods?

Comment on lines 117 to 118
let wasm_code: Vec<u8> = execution_environment.wasm_code.clone().into();
let data: Vec<u8> = transaction.data.clone().into();
Copy link
Contributor

Choose a reason for hiding this comment

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

You should be able to omit the clone calls here, and the conversions to Vec. VariableList<u8> implements Deref<Target=[u8]>.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Wow, very clever. Thanks for the catch, will update to this.

pub fn new(slot: u64) -> Self {
Self(slot)
}
}

/// Convert from a hex string to a Root
impl TryFrom<&str> for Root {
Copy link
Contributor

Choose a reason for hiding this comment

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

FromStr might be a more idiomatic approach, but I don't think this is incorrect.


fn try_from(s: &str) -> Result<Self, Self::Error> {
let vec = Vec::from_hex(s).map_err(|e| {
return format!("cannot convert string to hex: {:?}", e);
Copy link
Contributor

Choose a reason for hiding this comment

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

Converting an Error into a String loses a lot of contextual information. Generally it'd be better to either return the original error type, or convert it to this crate's error type if you don't want to leak dependencies.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the callout!

This may be me being fairly new to Rust, but I'm not sure I totally agree here. Certainly I agree that returning a string will make it so the underlying Error struct defined by the hex crate won't get passed along in the return value of try_from. That said, I think all the relevant context will be there, since the debug print will just print out the underlying error from the hex crate and include it in the try_from error message.

I don't feel strongly here at all, but to me it doesn't seem compelling to have the extra logic to either 1) define an Error type for the eth2/types library or 2) just return the underlying error from hex. Seems cleaner, simpler, and just as effective to have it as written.

Interested in reading your response on this :)

}
}

impl EEIndex {
Copy link
Contributor

Choose a reason for hiding this comment

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

EeIndex would be more standard.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Awesome, thanks, will change!

.get(ee_index)
.unwrap();
assert_eq!(ee_state, &expected_post_state);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

thanks for all the tests - helps see the interface.

Copy link

Choose a reason for hiding this comment

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

agreed

@jrhea
Copy link

jrhea commented Feb 13, 2020

This is fantastic. The code is super easy to follow and the tests demonstrate how the interface will be used. The simulation interface is definitely functional, but it would be convenient if I didn't necessarily have to know about SSZ and other spec types in order to interact with it.

For example, when calling create_shard_block() I have to know about the ssz type VariableList in order to create a ShardTransaction. You could make this easier by either changing the CreatShardBlock struct to this:

    pub struct CreateShardBlock {
        pub shard: Shard,
        pub shard_transactions: Vec<u8>,
    }

or providing a helper method called create_shard_transaction() that accepts some bytes and an index.

@gjtrowbridge
Copy link
Contributor Author

Closing this because all of this is included in #27

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.

4 participants