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

Accept genesis state and parameter overrides #123

Merged
merged 17 commits into from
Sep 20, 2018

Conversation

KPrasch
Copy link
Contributor

@KPrasch KPrasch commented Aug 19, 2018

What was wrong?

The genesis parameters, including the block gas limit is hardcoded, indicated by Issue #88.

How was it fixed?

Allow an optional dictionary of genesis parameter overrides to be passed into the backend's __init__.
Only existing default genesis parameters can be overridden, or a ValueError will be raised.

Cute Animal Picture

Cute animal picture

@pipermerriam
Copy link
Member

@KPrasch can you rebase this now that #122 has been merged.

@KPrasch
Copy link
Contributor Author

KPrasch commented Aug 20, 2018

Awesome, and done! Thanks @pipermerriam

@@ -259,6 +259,15 @@ def make_genesis_block():
"uncles": [],
}

if overrides is not None:
if not (all(bool(override in genesis_block) for override in overrides)):
Copy link
Member

Choose a reason for hiding this comment

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

It takes a litlte extra work but I think we can improve the error message below and the readability of this code.

allowed_fields = set(genesis_block.keys())
override_fields = set(overrides.keys())
unexpected_fields = tuple(sorted(override_fields.difference(allowed_fields)))
if unexpected_fields:
    raise ValueError(
        "The following invalid fields were supplied to override the genesis parameters: {0}.".format(unexpected_fields)
    )

def make_genesis_block():
return {
def make_genesis_block(overrides=None):
genesis_block = {
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 suggest using the name default_genesis_block. This will work well in conjunction with removing the mutation via update later in this function.

fields = ', '.join(genesis_block)
error = "Invalid genesis overrides; Valid parameters are {}".format(fields)
raise ValueError(error)
genesis_block.update(overrides)
Copy link
Member

Choose a reason for hiding this comment

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

Very minor but we prefer non-mutative approaches. You can use cytoolz.merge as follows.

from cytoolz import merge
genesis_block = merge(default_genesis_block, overrides)

@@ -505,8 +505,9 @@ def revert_to_snapshot(self, snapshot_id):
for log_filter in self._log_filters.values():
self._revert_log_filter(log_filter)

def reset_to_genesis(self):
self.backend.reset_to_genesis()
def reset_to_genesis(self, genesis_parameter_overrides=None, genesis_state_overrides=None):
Copy link
Member

Choose a reason for hiding this comment

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

I'm thinking that we should not expose this API at the ETHTester level and only support it at the backend level. It seems reasonable that some backends might not support custom genesis params and I think it is reasonable to require manual instantiation of the backend when you wish to override genesis parameters.

@@ -278,7 +288,7 @@ class PyEVMBackend(object):
chain = None
fork_config = None

def __init__(self):
def __init__(self, genesis_parameter_overrides=None):
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 it would be better to just require providing the fully merged genesis parameters. I believe that will simplify a lot of these apis, removing the concept of merging the overrides into the base parameters. We should however provide an API for generating the merged parameters to make this easy for users.

Maybe a classmethod or staticmethod on the main PyEVMBackend class?

class PyEVMBackend:
    @staticmethod
     def generate_genesis_params(overrides=None):
        ...

    @staticmethod
    def generate_genesis_state(overrides=None):
        ...

Then the user workflow would be something like:

from eth_tester import EthereumTester, PyEVMBackend
state = PyEVMBackend.generate_genesis_state(state_overrides)
genesis_params = PyEVMBackend.generate_genesis_params(genesis_overrides)
tester = EthereumTester(backend=PyEVMBackend(genesis_params, state)

it's a bit verbose, but not terrible and it should simplify the other APIs.

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 100% agree.

@KPrasch
Copy link
Contributor Author

KPrasch commented Aug 20, 2018

Thanks for the review - excellent feedback / ideas @pipermerriam - Ill make these changes

@KPrasch KPrasch changed the title Accept genesis parameter overrides [WIP] Accept genesis parameter overrides Aug 20, 2018
@KPrasch KPrasch changed the title [WIP] Accept genesis parameter overrides Accept genesis parameter overrides Aug 29, 2018
@KPrasch
Copy link
Contributor Author

KPrasch commented Aug 29, 2018

OK - Ready for another review 🤠

I kept the changeset mostly focused on genesis parameters. I included both staticmethods for generating custom genesis params, and a classmethod: PyEVM.from_genesis_overrides as an alternate constructor. Parameter merging is non-mutative, and the overriding API is used at the backend level, for simplicity.

@@ -605,7 +605,7 @@ def create_log_filter(self, from_block=None, to_block=None, address=None, topics

if is_integer(raw_from_block):
if is_integer(raw_to_block):
upper_bound = raw_to_block
upper_bound = raw_to_block + 1
Copy link
Contributor

Choose a reason for hiding this comment

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

I thought I had fixed this in #121.
How did this re-appear?

Copy link
Contributor

Choose a reason for hiding this comment

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

does this PR need a rebase?

Copy link
Contributor Author

@KPrasch KPrasch Aug 29, 2018

Choose a reason for hiding this comment

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

Hello!

I reset this module to the wrong ref when this PR was a WIP. I rebased a fix.
Good catch, and Thanks for the review. 🤠

@KPrasch
Copy link
Contributor Author

KPrasch commented Sep 5, 2018

Checking In - Does this PR seem like a reasonable approach, or do I need make additional modifications?

@voith
Copy link
Contributor

voith commented Sep 16, 2018

@carver @pipermerriam Can you please review this? This is a very useful feature

Copy link
Contributor

@carver carver left a comment

Choose a reason for hiding this comment

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

Cool, looks like it's nearly there!

@@ -119,14 +120,14 @@ def get_default_account_keys():


@to_dict
def generate_genesis_state(account_keys):
def generate_genesis_state(account_keys, overrides=None): # TODO: Merge genesis state overrides
Copy link
Contributor

Choose a reason for hiding this comment

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

If you don't intend to do the genesis state changes in this PR, can you:

  • remove the TODO
  • open an issue (suggesting this approach)
  • remove this overrides keyword arg, which implies that the feature works

return get_default_genesis_params(overrides=overrides)

@staticmethod
def generate_genesis_state(overrides=None):
Copy link
Contributor

Choose a reason for hiding this comment

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

Another cleanup: delete overrides

@staticmethod
def generate_genesis_state(overrides=None):
account_keys = get_default_account_keys()
return generate_genesis_state(account_keys=account_keys, overrides=overrides)
Copy link
Contributor

Choose a reason for hiding this comment

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

delete overrides

return generate_genesis_state(account_keys=account_keys, overrides=overrides)

@classmethod
def from_genesis_overrides(cls, parameter_overrides=None, state_overrides=None):
Copy link
Contributor

Choose a reason for hiding this comment

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

delete state_overrides

@classmethod
def from_genesis_overrides(cls, parameter_overrides=None, state_overrides=None):
params = cls.generate_genesis_params(overrides=parameter_overrides)
state = cls.generate_genesis_state(overrides=state_overrides)
Copy link
Contributor

Choose a reason for hiding this comment

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

delete state_overrides

pass

def test_override_genesis_parameters(self, eth_tester):
super().test_reset_to_genesis(eth_tester)
Copy link
Contributor

Choose a reason for hiding this comment

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

It strikes me as odd to run a test directly, especially inside another test. Maybe make a helper function that does a genesis reset, which both tests call.


# Or use the alternate constructor
pyevm_backend = PyEVMBackend.from_genesis_overrides(parameter_overrides=param_overrides)
assert pyevm_backend.chain.header._gas_limit == 4745362
Copy link
Contributor

Choose a reason for hiding this comment

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

These two lines should probably be pulled out into a separate test, so that pyevm_backend from line 43 gets tested in the EthereumTester integration below.


# Initialize pyevm backend with genesis params
pyevm_backend = PyEVMBackend(genesis_params)
assert pyevm_backend.chain.header._gas_limit == 4745362
Copy link
Contributor

@carver carver Sep 17, 2018

Choose a reason for hiding this comment

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

How about using a public API instead, in case this private variable gets refactored? Something like:

genesis_block = pyevm_backend.get_block_by_number(0)
assert genesis_block.header.gas_limit == 4750000

@pipermerriam pipermerriam dismissed their stale review September 17, 2018 19:56

letting Jason take this.

@KPrasch
Copy link
Contributor Author

KPrasch commented Sep 18, 2018

@carver - Thanks for the review - I'd like to finish the state overrides also, which I just pushed.

Copy link
Contributor

@carver carver left a comment

Choose a reason for hiding this comment

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

Sweet. Any thoughts on how important it is to be able to have different account state in two different accounts in the same genesis?

Like account A has balance 1, and B has balance 2. If that's likely to be important, let's figure out a good API for it now.

cc @voith

genesis_params = get_default_genesis_params()

if genesis_state:
accounts = len(genesis_state)
Copy link
Contributor

Choose a reason for hiding this comment

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

If you like, num_accounts could be a little clearer as a name.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

✔️

return get_default_genesis_params(overrides=overrides)

@staticmethod
def generate_genesis_state(overrides=None, accounts=None):
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems likely to cause confusion to have two methods of the same name (generate_genesis_state), especially in the same file. Maybe the other method could be called generate_genesis_state_for_keys.

Also, num_accounts here would be clearer as a variable name.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

✔️

return generate_genesis_state(account_keys=account_keys, overrides=overrides)

@classmethod
def from_genesis_overrides(cls, parameter_overrides=None, state_overrides=None, accounts=None):
Copy link
Contributor

Choose a reason for hiding this comment

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

I count three new APIs that are different ways of accomplishing similar things, maybe four if you count the generate_genesis_state_for_keys method. That means a lot of new documentation and examples and helping people figure out which one they should use in which situation. Any way we can tighten this up a bit? Ideally there would be one preferred way of setting genesis info.

For example, maybe this method shouldn't exist, and if someone wants to override both parameters and state, then the preferred way would be explicitly, like:

params = PyEVMBackend.generate_genesis_params(overrides=parameter_overrides)
state = PyEVMBackend.generate_genesis_state(overrides=state_overrides, accounts=accounts)
backend = PyEVMBackend(genesis_parameters=params, genesis_state=state)

That would be one less method to document, and one less question for where a new user should start when they want to override genesis info. Sure, it's three lines instead of one, but they decompose nicely, into understanding the other public APIs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Personally, I like the simplicity of a one-liner in this situation - It even reduces confusion in some cases, IMO. I also understand that this does add an opportunity for confusion, and additional maintenance.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

✔️

@KPrasch
Copy link
Contributor Author

KPrasch commented Sep 18, 2018

Having multiple states specified may indeed be useful. Perhaps if the state_overrides dictionary has more than one element, we can create an equal number of accounts, mapping the overrides to each new account's state (although order will not be preserved in < python 3.6).

@KPrasch KPrasch changed the title Accept genesis parameter overrides Accept genesis state and parameter overrides Sep 18, 2018
@carver
Copy link
Contributor

carver commented Sep 18, 2018

Having multiple states specified may indeed be useful.

Cool, how about something like this API...

This creates three account duplicate states, with a balance of 10**20:

state = PyEVMBackend.generate_genesis_state_duplicates(overrides=dict(balance=10**20), num_accounts=3)

This creates 4 accounts, the first with balance 10**20, the second with nonce 2, and two more default accounts:

state = PyEVMBackend.generate_genesis_state(override_accounts=(
  dict(balance=10**20),
  dict(nonce=2),
  {},
  {},
))


from eth_tester import (
EthereumTester,
PyEVMBackend,
)
from eth_tester.backends.pyevm.main import generate_genesis_state_for_keys, get_default_account_keys, get_default_genesis_params
Copy link
Contributor

Choose a reason for hiding this comment

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

can you wrap this line?
too bad that eth-tester doesn't have isort!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

✔️

@voith
Copy link
Contributor

voith commented Sep 19, 2018

Any thoughts on how important it is to be able to have different account state in two different accounts in the same genesis?

I can't think of an immediate use case where I'd need different account states. However, having functionality that allows replicating an actual genesis block would be awesome!

Although what I would really love to see happen is that there should be an optional way to just provide an account address instead of the backend generating one for you. This helps when you're trying to sign a transaction and you have the private key of any genesis account. Currently, I do this in a very hacky way i.e I keep a mapping of privatekeys and default genessis accounts.

@voith
Copy link
Contributor

voith commented Sep 19, 2018

overall great work @KPrasch! I am really looking forward to use this feature. Currently I have to monkeypatch some eth-tester code, specifically the gasLimit .

@carver
Copy link
Contributor

carver commented Sep 19, 2018

However, having functionality that allows replicating an actual genesis block would be awesome!

You mean pass in a json-encoded string (the genesis file) to some classmethod, and you get a PyEVMBackend instance with that genesis block? I like it.

Maybe that should even be our primary public API, and these PR's methods are the tools we use to implement it. It seems like that API requires the least custom knowledge about eth-tester, since the genesis file format is a (de facto) global standard.

@voith
Copy link
Contributor

voith commented Sep 19, 2018

You mean pass in a json-encoded string (the genesis file) to some classmethod, and you get a PyEVMBackend instance with that genesis block?

Yeah, I got this idea from pyethereum: https://github.com/ethereum/pyethereum/blob/develop/genesis_frontier.json

This will solve my account handling problem too.

@carver
Copy link
Contributor

carver commented Sep 19, 2018

Cool, so I'm imagining an API like

backend = PyEVMBackend.from_genesis({'gas_limit': ... })

# also accepts the equivalent json-encoded string:
backend = PyEVMBackend.from_genesis('{"gas_limit": ... }')

The biggest problem I see with the approach is the way it handles hosted keys. Basically, if you specify a genesis file, then there's no way to also have PyEVMBackend host your keys for you. You would always have to pre-sign the transactions.

Some options:

  1. Suck it up: if you want to specify a genesis file, then you need to manage your own private keys (which isn't too bad thanks to web3.py, but still more setup work than the alternative)
  2. Add extra hosted accounts: this makes the API a little messier, and kind of pollutes the idea that you get exactly the genesis file you specified, but it is an easy way to add hosted keys. Also, it doesn't have include a clean API for custom state on your hosted keys. For that, refer back to "Suck it up". :)

Example of option 2:

backend = PyEVMBackend.from_genesis({'gas_limit': ... }, num_hosted_accounts=10)

num_hosted_accounts would default to 0. So if you don't specify hosted accounts, you don't get any. Then you would get the exact same starting state root as any other node that imports a genesis file. If num_hosted_accounts > 0, then from_genesis() would add additional accounts to the starting state with plenty of balance, and keep the private keys for you. (as it does now)

I'm on the fence about these two. I suppose option 1 is a slight favorite until we have a good reason. It's much easier to expand an API then to go through a deprecation/major-version cycle to remove it. :)

@voith
Copy link
Contributor

voith commented Sep 20, 2018

I'm inclined towards option 1 aswell. I had always imagined this feature this way, but I only read the code in this PR yesterday. Although, this takes away alot of the convenience from the user, eth-tester IMO is a low level library and user trying to use it can be assumed to know a little about a genesis block. If this feature is documented well enough then I don't see any problem with having it.

@KPrasch If you make this change then I can promise to write the docs.

@jMyles
Copy link
Contributor

jMyles commented Sep 20, 2018

This has turned into quite an endeavour!

I don't have any specific opinions on the underlying API change beyond simple hope and support, but I do want to echo @voith in pointing out that @KPrasch (a close friend and colleague of mine) has done some nice work here that improves the python ethereum ecosystem in a small but very meaningful way.

And also, thanks @voith for committing to writing the docs.

Whichever option is selected, the remaining work is non-trivial, so here's hoping for clear thoughts and an easy merge for everyone involved.

@KPrasch
Copy link
Contributor Author

KPrasch commented Sep 20, 2018

Awesome discussion and support, Thanks All! :-) 🛩️

With regard to creating genesis state mapping for accounts:

Cool, how about something like this API...

I like the Ideas presented above; Here is another Idea of how to handle injecting account states while avoiding usage of an empty dictionary to represent the default state:

override_accounts = (
  dict(balance=10**20),
  dict(nonce=2)
)
state = PyEVMBackend.generate_genesis_state(override_accounts, num_accounts=4)


#  Creates 4 accounts: two overridden states, and two default states
#  Raises ValueError if `num_accounts < len(override_accounts)`

This is very sensible, IMO:

state = PyEVMBackend.generate_genesis_state_duplicates(overrides=dict(balance=10**20), num_accounts=3)

Although, I can also imagine an even more dynamic usage of PyEVMBackend.generate_genesis_state to hand this case, too.

With regard to PyEVMBackend.from_genesis

For now, I think preserving the idea that you get exactly what the genesis file depicts is paramount. (Option 1) Web3.py provides sufficient tooling to handle the local keys.

It's much easier to expand an API then to go through a deprecation/major-version cycle to remove it. :)

It may be possible to build out more features using this tooling in the future. Also worth mentioning, back in Issue #88 there was discussion of restoring state from a snapshot, too.

And also, thanks @voith for committing to writing the docs. 📖

@carver
Copy link
Contributor

carver commented Sep 20, 2018

If you make this change then I can promise to write the docs.

Great, thanks @voith !

Also worth mentioning, back in Issue #88 there was discussion of restoring state from a snapshot, too.

Yes, thanks for the callout. Snapshot & Restore belongs in a new issue, as Piper mentioned. (All of us can aim for more front-loaded discussion about the API this time :)) -- I just made #130 for it.

KPrasch has done some nice work here that improves the python ethereum ecosystem in a small but very meaningful way.

Totally agreed @jMyles !

Whichever option is selected, the remaining work is non-trivial, so here's hoping for clear thoughts and an easy merge for everyone involved.

Yup, I'm 99% of the way with you. The 1% -- I don't want to rush into a new public API (which has lasting maintenance effects) just because we want to merge in this contribution.

@KPrasch How about prefixing the PyEVMBackend methods with _ to further indicate that we may be iterating on them shortly? With that I would be 100% eager to merge:

  • _generate_genesis_params()
  • _generate_genesis_state()
  • _reset_to_genesis()

@KPrasch
Copy link
Contributor Author

KPrasch commented Sep 20, 2018

Yup, I'm 99% of the way with you. The 1% -- I don't want to rush into a new public API (which has lasting maintenance effects) just because we want to merge in this contribution.

No Rush, although I do think this may be well suited as multiple PRs!

How about prefixing the PyEVMBackend methods with _

Yes - mostly easy, except for the call in EthereumTester.reset_to_genesis

@carver
Copy link
Contributor

carver commented Sep 20, 2018

I do think this may be well suited as multiple PRs!

For sure, #129 and #130 to track...

except for the call in EthereumTester.reset_to_genesis

Ah yes, right 👍


Thanks again for exploring this with us and being flexible!

@carver carver merged commit a9ae913 into ethereum:master Sep 20, 2018
@KPrasch
Copy link
Contributor Author

KPrasch commented Sep 20, 2018

Thank You!

@voith
Copy link
Contributor

voith commented Sep 28, 2018

@KPrasch @jMyles Sorry for the silence. This got lost in the countless emails I get daily.

@KPrasch (a close friend and colleague of mine) has done some nice work here that improves the python ethereum ecosystem in a small but very meaningful way.

Agreed! I didn't have anything against this PR. Its just that while discussing, we came up with a different solution which solve a lot of use cases. Just didn't want the underlying API to be changed later, which would mean backwards incompatgibilty in the future.

And great that @carver merged this PR because I'm not sure how soon any of us will implement the newly discussed API.

I'll just wait for ethereum/py-evm#1299 to land.

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.

None yet

5 participants