-
Notifications
You must be signed in to change notification settings - Fork 333
feat: pass StateOracle as a parameter to state_transition
#1371
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
base: forks/osaka
Are you sure you want to change the base?
Conversation
src/ethereum/osaka/fork.py
Outdated
def state_transition(chain: BlockChain, block: Block, oracle=None) -> 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.
main TLDR; the object that is used to fetch state is now an explicit parameter to the state transition function instead of it being implicit
src/ethereum/osaka/fork.py
Outdated
sender_account = get_account(block_env.state, sender) | ||
sender_account = block_env.oracle.get_account(sender) |
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.
Most changes look like this:
Before:
func(state, params)
After:
oracle.func(params)
# Use generic types for compatibility across forks | ||
Account = Any | ||
Address = Bytes20 |
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 initially thought that this interface would fork agnostic, but perhaps it makes sense to have one per fork?
This is required for SSTORE gas calculations per EIP-2200. | ||
The implementation should use state snapshots/checkpoints to track | ||
pre-transaction values. | ||
TODO: The oracle does not have a `begin_transaction` method, so it kind of breaks 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.
Note: TODO
from ethereum_types.bytes import Bytes20, Bytes32 | ||
from ethereum_types.numeric import Uint | ||
|
||
# TODO: This file is current Osaka specific -- we could move state.py into here to mitigate 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.
Note: TODO
cc @SamWilsn @gurukamath since you were tagged in that issue -- would be good to know if this is directionally what you were thinking first, since there is likely some cleanup required |
def get_storage( | ||
self, address: Address, key: Bytes32 # noqa: U100 | ||
) -> Bytes32: # noqa: U100 | ||
"""Get storage value at `key` for the given `address`.""" |
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.
noqa: U100 was needed for tox lint
def account_has_code_or_nonce( | ||
self, address: Address # noqa: U100 | ||
) -> bool: # noqa: U100 | ||
""" | ||
Check if account has non-zero code or nonce. | ||
|
||
Used during contract creation to check if address is available. | ||
""" | ||
|
||
def account_has_storage(self, address: Address) -> bool: # noqa: U100 | ||
""" | ||
Check if account has any storage slots. | ||
|
||
Used during contract creation to check if address is available. | ||
""" |
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.
As noted in the PR description; methods like these can be moved into something like an "EVMOracle", since they can be implemented using get_account plus a few extra LOC to check if there is code/nonce/storage.
The user would then only implement MerkleOracle, which then gets wrapped in an EVMOracle so that the EVM can use the relevant methods
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.
This is the motivation behind the minimal state interface in #1209. Everything that isn't off the shelf that can be done in the specs should be done there, even if the performance is terrible.
@@ -19,7 +19,6 @@ | |||
from ethereum.utils.numeric import ceil32 | |||
|
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.
blockhash was not modified, but that should also likely be a part of "state" since 2935 has been implemented
# TODO: Hack imo | ||
if self.fork.is_after_fork("ethereum.osaka"): | ||
from ethereum.state_oracle import MemoryMerkleOracle | ||
|
||
oracle = MemoryMerkleOracle(self.alloc.state) | ||
state_or_oracle = {"oracle": oracle} | ||
else: | ||
state_or_oracle = {"state": self.alloc.state} |
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.
Since the change currently only applies to Osaka
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.
There are quite a few places in the ethereum_spec_tools
directory that rely on accessing the state parameter explicitly, so I'll wait to see if this is directionally correct before modifying those
Our primary concern is readability, and I fear feeding the state oracle through everything will hurt that. Would an approach like our trace implementation work? Otherwise I think this looks pretty good! |
I have no strong opinion on how its implemented :) -- when you say feeding the state oracle through everything, do you mean places outside of BlockEnv? Since in most places we are replacing |
6604ee8
to
c07b175
Compare
@SamWilsn have added a commit to an approach that is closer to evm_trace; with a global oracle -- was this what you had in mind? |
# TODO: Remove this, since its not being used. | ||
"state": self.alloc.state, |
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.
TODO
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.
Need to remove state from BlockEnvironment to finish this
return old | ||
|
||
|
||
def get_state_oracle() -> MerkleOracle: |
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.
You're still returning an object here. We want this to be as flat as possible. For each function on the MerkleOracle
, create a function in this file exposing it.
if _state_oracle is None: | ||
raise RuntimeError( | ||
"No global state oracle set. Call set_state_oracle() first." | ||
) |
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.
There should always be a default. That way the reader can easily trace the implementation of the unmodified spec. The average reader isn't going to be going through the whole codebase (instead just reading a function or implementing an EIP), so locality is very important.
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 think this implies that the state_oracl folder will be copied between forks, unless we have a fork agnostic default
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 also think that we would need some state to create the default oracle with
Address = Bytes20 | ||
|
||
|
||
class MerkleOracle(Protocol): |
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, so we have a vague rule that if something changes between forks, it must be placed into the fork's folder. For example, keccak256
is used in every fork and isn't likely to disappear, so it lives in ethereum.crypto
, where as Address
might get bumped to 32 bytes in the future, so it's defined per fork.
If the MPT is going to get replaced (or is fairly likely), it might be best to define it per fork.
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.
related to #1371 (comment)
self, address: Address # noqa: U100 | ||
) -> Account: # noqa: U100 |
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 think if you raise NotImplementedError
it mutes the linter, but I'm not 100% sure on that.
Address = Bytes20 | ||
|
||
|
||
class MerkleOracle(Protocol): |
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.
"Oracle" is a bit of a foreign term for non-cryptography folks. The base protocol could just be State
.
That said, we've managed to keep protocols and most OOP out of the spec so far, so we might want to rethink how we organize this.
def account_has_code_or_nonce( | ||
self, address: Address # noqa: U100 | ||
) -> bool: # noqa: U100 | ||
""" | ||
Check if account has non-zero code or nonce. | ||
|
||
Used during contract creation to check if address is available. | ||
""" | ||
|
||
def account_has_storage(self, address: Address) -> bool: # noqa: U100 | ||
""" | ||
Check if account has any storage slots. | ||
|
||
Used during contract creation to check if address is available. | ||
""" |
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.
This is the motivation behind the minimal state interface in #1209. Everything that isn't off the shelf that can be done in the specs should be done there, even if the performance is terrible.
Discussed with @SamWilsn offline and this may not be strictly better than what is already on main wrt the design constraints for the execution specs. Putting this back in draft. The main change I'm looking to add is state tracking, which this should have made easier. If not, we can close it and make a separate PR for state tracking |
What was wrong?
Not mergable in current state
Related to #1209
How was it fixed?
This initial PR is made to start discussion on implementing this. Something like this would be useful for both:
The idea being that an oracle/state object can be wrapped to record whenever state has been accessed/modified.
Notes
To get what the original PR described, I believe we would move most of the methods into something like an
EVMOracle
structure that depends on aMerkleOracle