-
Notifications
You must be signed in to change notification settings - Fork 654
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
EIP-1283 #1410
EIP-1283 #1410
Conversation
69b84f1
to
20e3d44
Compare
Note to self (from spec):
|
eth/vm/logic/storage.py
Outdated
|
||
gas_refund = 0 | ||
|
||
if current_value == value: |
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 whole block looks like an excellent candidate for a stand-alone utility function which can be tested in isolation.
Also, if we convert all of the inputs to booleans:
def get_gas_cost_and_refund(original_is_zero, current_is_zero, value_is_zero, original_equals_current, original_equals_value):
...
We end up with a function that has 2**5 -> 32
possible input combinations for which we should be able to explicitely specify what the expected costs are.
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.
Unfortunately though, following the spec causes negative refunds for some configurations for us. I believe the reason for this is:
If an implementation uses “execution-frame level” refund counter (a new refund counter is created at each call frame, and then merged back to parent when the call frame finishes), then the refund counter needs to be changed to signed – at internal calls, a child refund can go below zero.
This seems to apply to us. So, if you refund counter would be a single instance per transaction, then we would not get negative refunds, but since our refund counter is per Computation/Message, we get negatives.
That doesn't contradict what you wrote, we could just assert getting negative results for some of the combinations. I still have to taker a closer look to see what would be the best way forward.
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 that implies that our GasMeter
may need to be changed since it doesn't allow for negative refunds. And likely we just need to create a new ConstantinopleGasMeter
since the logic isn't needed for pre-constantinople forks.
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'm trying out something more compositional right now but I'm not sure if I like it because I think it doesn't play so well with the rest of our architecture (e.g. we do not use a DI framework which takes the boilerplate out of composition). So I may fall back to what you suggest but I want to give it a chance at least.
20e3d44
to
37ed35e
Compare
) | ||
|
||
|
||
def sstore_eip1283(computation): |
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 moved this out into constantinople/storage.py
because I strongly feel cluttering our main code base with fork specific logic is wrong. We currently have a bit of a mix (with the majority of fork specific logic being nicely isolated in vm/forks/*
) Kinda related to our discussion we had in #1146
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'm completely onboard with this. I've got a code reorganization building up in my head to move all of the EVM stuff under eth.evm
the same way the beacon chain stuff is under eth.beacon
, which would also meaning moving a lot of stuff thats in eth.validation
, eth.constants
, eth.rlp
etc around a bit to match.
def __init__(self, start_gas: int) -> None: | ||
def __init__(self, | ||
start_gas: int, | ||
refund_strategy: RefundStrategy = default_refund_strategy) -> 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.
@pipermerriam @carver I wanted to avoid ConstantinopleGasMeter
and reached for a more compositional approach that passes in a RefundStrategy
. I don't have strong feelings about it. If you rather like this to follow the path that the rest of the code base uses (introducing ConstantinopleGasMeter
and overwriting refund_gas
) that is cool with me as well. In general, I'm more a friend of the compositional approach, it's just that this change is so tiny that I think there isn't much harm from an inheritance approach either.
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.
Yeah, seems promising, and a reasonable place to experiment with the approach because it's so simple. 👍
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.
Some thoughts on the composition approach.
eth/vm/gas_meter.py
Outdated
meter.gas_refunded - amount, | ||
amount, | ||
meter.gas_refunded, | ||
) |
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 these logs can stay in the GasMeter
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 was wondering about that and then said to myself: If it's really just this single line, why not duplicate it to allow entirely swapping out the implementation including the generated log.
def __init__(self, start_gas: int) -> None: | ||
def __init__(self, | ||
start_gas: int, | ||
refund_strategy: RefundStrategy = default_refund_strategy) -> 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.
Yeah, seems promising, and a reasonable place to experiment with the approach because it's so simple. 👍
eth/vm/gas_meter.py
Outdated
|
||
|
||
def allow_negative_refund_strategy(meter: "GasMeter", amount: int) -> None: | ||
meter.gas_refunded += amount |
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 it raises a flag for me when there is a mutual dependency (GasMeter
on the refund strategy and refund strategy on GasMeter).
One alternative:
def allow_negative_refund_strategy(previous_refund_total: int, amount: int) -> None:
return previous_refund_total + amount
Which makes the strategy both stateless and side-effect-free.
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 thought about that but it is limiting the usefulness heavily. E.g. what if you want to implement a strategy that throws if e.g. gas_refunded
or gas_remaining
is above some treshold (just making things up). Sure, we could pass these as well but at this point we may just pass the entire meter and let the function inspect whatever it wants?
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.
Ok, I'm not married to that particular alternative, but the mutual dependency seems fairly undesirable. If there are no other options come to mind, then we can just drop the compositional approach.
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.
Well ok, maybe designing this in the most flexible way shouldn't be our main priority for now. We can design it in the stateless way you suggest as it is flexible enough to fulfill our current needs and if we ever come across the need for an alternative refund strategy that this system can't handle we can reconsider.
eth/vm/gas_meter.py
Outdated
amount, | ||
self.gas_refunded, | ||
) | ||
return self.refund_strategy(self, amount) |
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.
Then this would be something like:
return self.refund_strategy(self, amount) | |
self.gas_refunded = refund_strategy(self.gas_refunded, amount) | |
self.logger.trace( | |
'GAS REFUND: %s + %s -> %s', | |
self.gas_refunded - amount, | |
amount, | |
self.gas_refunded, | |
) | |
return self.gas_refunded |
37ed35e
to
e555f2a
Compare
@@ -2,5 +2,11 @@ | |||
|
|||
|
|||
GAS_EXTCODEHASH_EIP1052 = 400 | |||
GAS_SSTORE_NOOP_EIP1283 = 200 |
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.
It'd be ideal if the EIP1283
namespace was located before the parts that are different.
GAS_SSTORE_EIP1283_INIT = ...
GAS_SSTORE_EIP1283_CLEAN = ...
Makes the namespace more effective imho.
eth/db/account.py
Outdated
def _get_account(self, address): | ||
rlp_account = self._journaltrie.get(address, b'') | ||
def _get_account(self, address, from_journal=True): | ||
rlp_account = (self._journaltrie if from_journal else self._trie).get(address, b'') |
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 self._trie_cache
is preferable to self._trie
here, for performance.
Also, this approach might stop working once Byzantium+ VMs are optimized to only modify the trie after every block (instead of every transaction). But this is clean for now, so we can save the problem for later.
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.
Good catch!
if original_value == 0: | ||
gas_cost = constants.GAS_SSTORE_INIT_EIP1283 | ||
else: | ||
gas_cost = constants.GAS_SSTORE_CLEAN_EIP1283 |
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.
The name clean wasn't immediately meaningful to me. (It's also tough, because I wasn't sure if it was a verb or an adjective, and if it was an adjective, what noun is it modifying). I had to cross check the EIP a few times. I guess this means you are storing into a not-dirty slot.
Some options to replace CLEAN:
FIRST_CHANGE
MAKE_DIRTY
TO_CLEAN_SLOT
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 looked into geth
and ethereum-js
and I think one more and they all used these names, so I just copied them from there. My preference would be to stick to that name just for the sake of sharing the vocabulary with them.
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.
Fair enough, convention vs clarity is a tightrope walk. I feel ambivalent about this one. If it were my PR, I'd probably pick the name that I thought was clearest and add a comment in the constant file to reference the conventional name. But I don't feel strongly enough to try to convince you to change it. :D
e555f2a
to
dec4137
Compare
What was wrong?
Need to implement EIP-1283 #1105
How was it fixed?
SSTORE
opcode in constantinopleGasMeter
to allow passing aRefundStrategy
to allow negative refunds (will balance out within transaction. That said, I wonder if we should introduce a new additional check that raises if it does not balance out on the tx levelCute Animal Picture