-
Notifications
You must be signed in to change notification settings - Fork 2.6k
[RFC] srml-contracts: Remove ext_return #3038
Comments
A couple of thoughts:
|
Good points. I agree that it is not worth guaranteeing that gas won't be overcharged in case of a trap, since it would just be impractical. But I do definitely think it is a good property to guarantee that gas won't be overcharged if the contract call exits in an expected way, which we cannot guarantee right now due to this problem. This is also why I think that expected errors should be indicated by a clean return of a non-zero status code (like in C or something) vs being signaled by a trap. This can still result in a reversion of any state changes, but one could return an error message for diagnostics (#2082) or indicating a precondition failure vs some unexpected exception/panic. On that note, I don't think it would be a crazy idea to consume all remaining gas on a trap, provided there's a way to revert the transaction without trapping, ie. by returning a non-zero exit status. This would match the behavior of the EVM (see rationale for EIP 140). With regards to point 4., while it does map to Rust's error handling, I think integer exit code + data buffer is compatible with lots of different programming language error handling patterns. Certainly C, exceptions, etc. |
Indicating errors this way adds additional overhead (in terms of code size and consumed gas) though and requires special cooperation from the language/framework side. I.e you will have to express to return error this way, whereas traps allow implementation of both approaches. There is also a downside that error handling code tends to be least tested so I think from the security perspective it might be a better idea to try to minimize the amount of code on such path.
Yes, sure it is compatible. My point was just let's not focus on Rust or any particular language. As with my previous point, trapping allows both immediate termination and implementation that looks at the return code thus it is more universal which is a desirable property for a low-level platform.
I guess rationale for EIP140 was to actually allow non-consuming behavior whereas our situation is that we always used non-consuming and so doesn't apply to us, right? |
So do you think it is OK to overcharge gas in the case of an "expected" error? Like a precondition failure? Or as a more concrete example, some DEX contract where someone tries to fill an already-filled order due to a race? I think it would be best to guarantee that the amount of gas consumed here is correct. And if the error handing is implemented with some sort of special trap, then we need to handle diverging calls anyway.
I don't follow this argument. Like error handling code in a contract or error handling code in the contract language compiler? If it's in the contract itself, then the amount of Wasm generated doesn't seem like it should add additional surface area -- it's just a bug in the contract.
Yeah, I just meant that as a reference that the EVM consumes all gas by default. |
I kind of like the idea stated here and was also thinking about ways for ink! to work more in that direction. However, I do see some complications with losing semantics upon using the scratch buffer for that since an analyzer or optimizer would probably have a harder time to reason about the code using it for returning data instead of the explicit Still I think that returning error codes is also suitable for our purposes in the way of differentiating between different error types. For example in ink! we would want to build in contracts ( This not only goes with the spirit of Rust but many other languages, also such as C. TL;DR Also concerning the gas metering problems I think this could be solved when we use Lightbeam IR instead of Wasm directly since Lightbeam IR is organized a bit differently and for example wouldn't even allow for a call to |
@pepyakin and I discussed the following more concrete proposal offline: The function signature of Any data remaining in the scratch buffer when If the call traps for any reason (out of gas or otherwise), then the caller status code is set to 0x0100 (the smallest u32 greater than 8 bits), changes are reverted, and the return data is empty. The most-significant 24 bits of the callee status code are reserved for interpretation by the contracts runtime module and are masked off in order to obtain the caller status code. For example, one of these reserved bits might be a self-destruct flag, as a solution to #3254. The idea is that a contract that wants to self-destruct can transfer its remaining balance where ever it wants with Regarding deployment, this is not incompatible with This proposal also requires a new call to write to the scratch buffer. We can rename |
We went in the opposite direction: We doubled down on Rationale for that is that is is difficult for contracts to bubble up status codes because of wasms structured blocks. This also saves the gas necessary to execute all the |
Right now there's an issue with gas metering and
ext_return
. In the following block of code:Currently, we will charge gas at the top of the block for all three instructions in the block. However,
call 4
might invoke a function which [invokes a function which]* callsext_return
. In this case the user is charged for thei32.add
instruction which was never executed.This is because
ext_return
is special in how it affects control flow. Handling it properly could lead to a lot more gas metering calls being injected after any control block or call to a function that might invokeext_return
.So... I think we should discuss removing
ext_return
entirely. The main point ofext_return
seems to be returning the result of the contract's execution to the caller. Instead, thecall
function on a contract could return an integer status code (0 = success, 1 = some time of error, 2 = another type of error, etc), and whatever output data remains in the scratch buffer when the function returns is its output data. This means that contracts would have to explicitly clear the scratch buffer before returning if they do not want to return anything, as there may be garbage left in it. If the function traps, the final scratch buffer state is ignored.This makes sense to me because it maps pretty easily to a Rust function that returns
Result<Vec<u8>, ContractError>
, where the status code represents the variant type of theErr(ContractError(..))
or 0 forOk(..)
. Then it is the job of ink! to properly handle the final state of the scratch buffer.In short, removing
ext_return
simplifies the contract runtime by pushing some complexity into the contract DSL, ink!I'd be interested to any thoughts. @Robbepop @pepyakin
This is related to #2852.
The text was updated successfully, but these errors were encountered: