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

EIP-1283 #1410

Merged
merged 1 commit into from
Oct 22, 2018
Merged

EIP-1283 #1410

merged 1 commit into from
Oct 22, 2018

Conversation

cburgdorf
Copy link
Contributor

@cburgdorf cburgdorf commented Oct 18, 2018

What was wrong?

Need to implement EIP-1283 #1105

How was it fixed?

  • Overwritten SSTORE opcode in constantinople
  • Implemented it by following the spec as closely as possible.
  • changed GasMeter to allow passing a RefundStrategy 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 level
  • added tests

Cute Animal Picture

put a cute animal picture link inside the parentheses

@cburgdorf cburgdorf force-pushed the christoph/feat/eip-1283 branch 2 times, most recently from 69b84f1 to 20e3d44 Compare October 19, 2018 10:41
@cburgdorf
Copy link
Contributor Author

cburgdorf commented Oct 19, 2018

Note to self (from spec):

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, I believe, why some test cases are not working yet) Fixed


gas_refund = 0

if current_value == value:
Copy link
Member

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.

Copy link
Contributor Author

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.

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 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.

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'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.

)


def sstore_eip1283(computation):
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 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

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 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:
Copy link
Contributor Author

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.

Copy link
Contributor

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. 👍

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.

Some thoughts on the composition approach.

meter.gas_refunded - amount,
amount,
meter.gas_refunded,
)
Copy link
Contributor

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

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 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:
Copy link
Contributor

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. 👍



def allow_negative_refund_strategy(meter: "GasMeter", amount: int) -> None:
meter.gas_refunded += amount
Copy link
Contributor

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.

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 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?

Copy link
Contributor

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.

Copy link
Contributor Author

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.

amount,
self.gas_refunded,
)
return self.refund_strategy(self, amount)
Copy link
Contributor

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:

Suggested change
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

@@ -2,5 +2,11 @@


GAS_EXTCODEHASH_EIP1052 = 400
GAS_SSTORE_NOOP_EIP1283 = 200
Copy link
Member

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.

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'')
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 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.

Copy link
Contributor Author

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
Copy link
Contributor

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

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 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.

Copy link
Contributor

@carver carver Oct 23, 2018

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

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