-
Notifications
You must be signed in to change notification settings - Fork 686
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
Add PAYGAS opcode #285
Add PAYGAS opcode #285
Conversation
@vbuterin does the |
evm/logic/context.py
Outdated
|
||
|
||
def paygas(computation): | ||
gasprice = computation.stack.pop(type_hint=constants.UINT256) |
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.
naming nitpick
gas_price
for conformity.
evm/logic/context.py
Outdated
|
||
# Only valid if (1) triggered in a top level call and | ||
# (2) not been set already during this transaction execution | ||
if computation.msg.depth == 0 and computation.msg.paygas_gasprice == 0: |
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.
computation.msg.paygas_gasprice == 0
seems problematic because it disallows a gas price of zero which at least historically, has been allowed. Maybe use None
as the not-set value?
evm/vm/message.py
Outdated
return 0 | ||
|
||
@paygas_gasprice.setter | ||
def paygas_gasprice(self, gas_price): |
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.
probably good to add validation for this value here to be really sure that it's valid.
evm/vm/message.py
Outdated
|
||
@paygas_gasprice.setter | ||
def paygas_gasprice(self, gas_price): | ||
if self.paygas_gasprice == 0: |
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 seems like there should be a logic branch here which throws an exception (not a VMException
but something that will bubble) if the value is already set, because if I understand correctly, we shouldn't even try to set the value if it's already set.
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.
What about returning True
of False
in paygas_gasprice.setter
to indicate if the value is successfully set or not.
if self.paygas_gasprice is None:
self._paygas_gasprice = gas_price
return True
else:
return False
and in PAYGAS
opcode check against this returned value and push 1
or 0
to the stack to indicate the outcome of this PAYGAS
operation?
def paygas(computation):
...
if computation.msg.depth == 0 and computation.msg.paygas_gasprice(gas_price):
…
computation.stack.push(1)
else:
computation.stack.push(0)
It's like fail of value transfer in a call. It seems better to me that it does not result in failure of transaction but I'm not that confident about this. Could it be the situation that gas were paid in top level call but later during some calls to other contract, the contract trigger PAYGAS
again?
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 like making subsequent PAYGAS calls ineffective (ie. as is implemented now) and pushing 1 on success and 0 on failure.
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 my suggestion was not focused on VM semantics, but rather our python API semantics. Here's the situation I'm trying to avoid.
# somewhere we set the gas price.
msg.paygas_gasprice = 10
# elsewhere in the code, we try to set it again.
msg.paygas_gasprice = 100 # this will silently fail
What I would like is that the later case cannot silently fail, hence, suggesting that we raise an exception. Then, if we ever have a location in the code which tries to set the gas price when it's already set which doesn't explicitely handle whatever exception we raise, then we'll get a hard failure in our code, rather than a silent failure that only manifests later at some point when the gas accounting is off.
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 seems like there should be a logic branch here which throws an exception (not a VMException but something that will bubble) if the value is already set
@pipermerriam Sorry I don't quite get it. Can you elaborate more on the difference between VMException and something that will bubble?
Should this result in reverting of transaction? One possible scenario is that gas is first paid by the forwarding contract but later during child/grandchild call to other contracts PAYGAS
is triggered(accidentally or not) again. IMO it seems like a bad user experience and a problem for debugging if this is to result in reverting the transaction.
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.
Look here: https://github.com/ethereum/py-evm/blob/master/evm/computation.py#L316-L338
Anything inside of vm execution which throws any exception that is a subclass of VMError
gets handled there, resulting in computation.is_error
being True
.
What I'm proposing is something that throws something like a TypeError
or another custom exception that is not a VMError
subclass. This way, if we write code which sets msg.paygas_gasprice
without implementing error handling and the msg.paygas_gasprice
has already been set... an error will be thrown in such a way that it won't be caught in the linked code above.
Does that make sense?
f8c04b9
to
c0f5276
Compare
@pipermerriam Made some updates. Now the exception |
evm/exceptions.py
Outdated
@@ -150,3 +150,7 @@ class UnannouncedStateAccess(VMError): | |||
been specified in the transaction's read or write list, respectively. | |||
""" | |||
pass | |||
|
|||
|
|||
class PAYGASResetGasprice(Exception): |
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.
other name suggestions:
GasPriceAlreadySet
GasPriceOverwriteProtection
I'm thinking the inclusing of PAYGAS
in the name isn't needed and is a little visually confusing.
evm/logic/context.py
Outdated
|
||
# Only valid if (1) triggered in a top level call and | ||
# (2) not been set already during this transaction execution | ||
if computation.msg.depth == 0: |
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 way this is written without an else
clause this does nothing if triggered from depth > 0
. Is this intended? Should it push 0
onto the stack if depth > 0
?
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 are right. It should push 0
onto the stack if depth > 0
.
evm/logic/context.py
Outdated
# (2) not been set already during this transaction execution | ||
if computation.msg.depth == 0: | ||
try: | ||
computation.msg.paygas_gasprice(gas_price) |
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 looks like an error. You are calling a property which I believe should raise an exception stating that either NoneType
or the integers are not callable.
I do however like using a setter function rather than assigning to a property that throws an exception. Maybe rename to something like computation.msg.set_PAYGAS_gasprice(...)
?
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.
My mistake here. I will update the setter to set_PAYGAS_gasprice
function.
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.
Do you think it's worth implementing a few minimal test cases for this opcode to hit these code paths? I know the JSON test suite will cover these eventually, but
- we'd probably benefit from having basic test cases for opcodes that were not buried in the hard to decipher JSON fixture tests.
- until we have JSON fixture tests for this it'd be nice to know it works (from a testing/CI perspective)
- those test cases can be use to fill out whatever EIP is written to specify this opcode.
evm/vm/message.py
Outdated
return self._paygas_gasprice | ||
|
||
@paygas_gasprice.setter | ||
def paygas_gasprice(self, gas_price): |
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.
Continuation of previous suggestion to remove the setter
and just make this a method set_PAYGAS_gasprice
. Seems a bit more intuitive/correct/something.
evm/vm/message.py
Outdated
@property | ||
def paygas_gasprice(self): | ||
if self._paygas_gasprice is None: | ||
return 0 |
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 still curious about returning 0
instead of None
here. It seems valuable to be able to distinguish between not-set and set-to-0 which cannot be done in the current implementation.
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.
My rationale was that from inside(Message
object) the two cases are distinguished by msg._paygas_gasprice
and from outside one indeed can not tell the difference. But since when msg.paygas_gasprice
is used to compute gas charge/refund (from outside) the two cases are basically the same.
Also if msg.paygas_gasprice
could return None
, we need to add a condition check every time it's used to compute gas charge/refund.
gas_refund_amount = (gas_refund + gas_remaining) * computation.msg.paygas_gasprice if computation.msg.paygas_gasprice is not None else 0
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 i'm on the fence about this. Your call. Here's some extra thoughts on why it might still be good to use None
.
The Computation
object has an existing convention of having get_XXX
methods for things to account for these little bits of internal logic. I think the following API is consisten with the rest of the class and clean.
class Computation:
...
def get_PAYGAS_gas_price(self):
if self._paygas_gasprice is None:
return 0
else:
return self._paygas_gasprice
# and then used elsewhere
gas_refund_amount = (gas_refund + gas_remaining) * computation.get_paygas_gasprice()
It also has the nice benefit of not reaching down into the message object.
Again, your call. I'm +0 on this change if you don't think there is a valid use case for differentiating between set to 0 and unset.
c2be9cb
to
b47cfce
Compare
…dySet exception if gas price is already set
Since this had to be rebased and some part of this PR is already been taken cared of in other new PR, I just re-do it along with PR feedbacks. I changed it back so that I moved |
Another thing is transferring the transaction fee to def execute_transaction(self, transaction):
…
transaction_fee = (transaction.gas - gas_remaining - gas_refund) * PAYGAS_gasprice
computation.tx_fee = transaction_fee
…
def add_transaction(self, transaction, computation, block):
…
if block.tx_fee_sum:
block.tx_fee_sum += computation.tx_fee
else:
block.tx_fee_sum = computation.tx_fee
…
def finalize_block(self, block):
…
with self.state_db() as state_db:
state_db.delta_balance(block.header.coinbase, block.tx_fee_sum)
self.logger.debug(
"TOTAL TRANSACTON FEE: %s -> %s",
block.tx_fee_sum,
block.header.coinbase,
)
… |
def get_PAYGAS_gas_price(self): | ||
return self._paygas_gasprice | ||
|
||
def set_PAYGAS_gasprice(self, gas_price): |
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 this is a good move (moving the value and the setters co Computation
). This function could use one extra validation check to enforce depth == 0
within the setter itself.
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
Added the depth == 0
validation check along with PAYGAS testing contract fixture and test suite.
… them altogether in `finalize_block`
According to our last discussion, the |
@@ -228,6 +230,7 @@ def __call__(self, computation): | |||
value=value, | |||
data=b'', | |||
code=call_data, | |||
transaction_gas_limit=computation.msg.transaction_gas_limit, |
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.
FYI, this will need to be reworked to use the new TransactionContext
from #327 once it's complete and merged.
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.
Will do. 👍
evm/logic/system.py
Outdated
|
||
# Only valid if (1) triggered in a top level call and | ||
# (2) not been set already during this transaction execution | ||
if computation.msg.depth == 0: |
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.
Now that computation.set_PAYGAS_gasprice
has an internal check for the depth, what do you think about removing this outer if
statement and just adding another except
clause to the try/except/else
block?
evm/vm/forks/sharding/computation.py
Outdated
def set_PAYGAS_gasprice(self, gas_price): | ||
validate_uint256(gas_price, title="PAYGAY.gas_price") | ||
if self.msg.depth != 0: | ||
raise NotTopLevelCall("This should only happen in a top level call context.") |
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.
nitpick
I think the exception message would be clearer as:
"The
set_PAYGAS_gasprice
API is only valid when called from the top level computation"
Makes it a little clearer what "this" is.
evm/vm/forks/sharding/vm_state.py
Outdated
) | ||
with state_db_cm() as state_db: | ||
state_db.delta_balance(self.coinbase, transaction_fee) | ||
computation.tx_fee = transaction_fee |
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.
Rather than storing this value on the computation object what do you think about just re-calculating it within add_transaction
? It looks like we have all of the necessary information already available in the context of that function to do the computations.
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 are right. Totally makes sense.
tests/core/helpers.py
Outdated
@@ -32,7 +32,8 @@ def new_sharding_transaction( | |||
|
|||
[destination, value, msg_data, vrs]. | |||
""" | |||
tx_data = pad32(data_destination) + pad32(bytes([data_value])) + pad32(data_msgdata) + pad32(data_vrs) # noqa: E501 | |||
assert len(data_vrs) in (0, 65) | |||
tx_data = pad32(data_destination) + pad32(bytes([data_value])) + pad32(data_msgdata) + pad32(data_vrs[:32]) + pad32(data_vrs[32:64]) + bytes(data_vrs[64:]) # noqa: E501 |
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.
suggest reformatting to
tx_data = (
pad32(data_destination) +
pad32(bytes([data_value])) +
pad32(data_msgdata) +
pad32(data_vrs[:32]) +
pad32(data_vrs[32:64]) +
bytes(data_vrs[64:]
)
…rove its exception message
child_computation.output[:actual_output_size], | ||
) | ||
|
||
if not child_computation.should_burn_gas: |
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 to self.
this is a confusing if
statement because of the need for the not
. We might make a second property which returns the inverse of this as computation.should_return_gas
.
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.
Noted. Will make a PR to update this.
evm/vm/forks/sharding/vm_state.py
Outdated
gas_remaining = computation.get_gas_remaining() | ||
gas_refunded = computation.get_gas_refund() | ||
gas_used = transaction.gas - gas_remaining | ||
gas_refund = min(gas_refunded, gas_used // 2) |
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.
Hrm, rather than repeating this block of code both here and above, I think we should dig it out into a utility of some sort.
def compute_transaction_gas_amounts(computation, transaction_gas, gas_price):
gas_remaining = computation.get_gas_remaining()
gas_refunded = computation.get_gas_refund()
gas_used = transaction_gas - gas_remaining
gas_refund = min(gas_refunded, gas_used // 2)
tx_fee = (transaction_gas - gas_remaining - gas_refund) * gas_price
return tx_fee, gas_used, gas_refund
Something like this. Could also be implemented as a method on the Computation
class which would allow removing the gas_price
argument from the call signature.
evm/vm/forks/sharding/vm_state.py
Outdated
tx_fee = (transaction.gas - gas_remaining - gas_refund) * tx_PAYGAS_gasprice | ||
# Bookkeep this transaction fee | ||
if hasattr(block, "tx_fee_sum"): | ||
block.tx_fee_sum += tx_fee |
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'd like to figure out a way to do this which doesn't involve storing data on the block object. Looking around at the code however, it's not very clear where that would be.
For now, lets just formalize this property on the block object so that we don't have to do hasattr
checks.
What was wrong?
Implementation of #234
How was it fixed?
Add
PAYGAS
opcodetransaction_gas_limit
,paygas_gasprice
andpaygas_total_fee
inShardingMessage
objecttransaction_gas_limit
is the same astransaction.gas
paygas_gasprice
is set byPAYGAS
opcode and only under two conditions:computation.msg.depth == 0
)paygas_total_fee
is equal totransaction_gas_limit * paygas_gasprice
paygas_total_fee
from transaction initiator(transaction.to
)'s balancepaygas_gasprice
instead oftransaction.gas_price
0x5c
0xf5
, placed inStack, Memory, Storage and Flow OperationsSystems section. I preferred to put it in Environment Information section but it's full already.execute_transaction
andtransfer the fee directly tostore the tx fee amount due inPAYGAS_TEMPORARY_ACCOUNT
of which the account data is enforced to be permanently stored in database hence it's witness data is not required.block.tx_fee_sum
and transfer all fees to coinbase infinalize_block
function2. Add a function that generates a transaction to transfer fees collected from all transactions of this block to coinbase.gas_price
away, removing it from transaction format.(Moved to another PR)Cute Animal Picture