-
-
Notifications
You must be signed in to change notification settings - Fork 810
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
VIP: Cost and usability Improvements to internal function calls #901
Comments
I do find myself avoiding internal calls in favor of reusing the same code in multiple places to save gas, which is probably not ideal. I don't personally know the tradeoffs well but If this can be done safely I'm all for it. |
I get your point but I don't fully agree with that. Unlike other software, smart contracts' code must be explicit and easily readable. In most other cases, it's more important to not repeat yourself in the code but in smart contracts, I think readability of the function should be more important.
I am in favor of your fourth proposal of creating the I would like to hear more opinions on that as code repetition is an insecure thing as well so I'm not completely sure what's more important yet, but I believe there's a room for improvement. |
Idk if I agree that internal function calls have the same readability issues as Solidity modifiers. Also code repetition can feel very verbose and reduce readability. If you need to do a square root in five different functions, its probably bad to have the sqrt() implemented five times. But people will be tempted to do so if they save 1000 gas per function call. Discouragement through high gas cost makes sense on a protocol level (ie making sstore exepensive) but feels wrong for smart contract languages. To be clear I don't think
is a big problem. I do however think
is bad. If using JUMP for internal calls can be done as safely as CALL, it should be used. I don't know the security tradeoffs at all, hopefully someone who does can chime in here! |
It's not only about readability, but maintainability as well. More mistakes will be made if 5 pieces of code have to change. Take a look at these There are a total of 12 lines of code in the bodies, which can be written once, then reused 3 times in the standard. If they were written 3 times over, it would be very hard (for my brain at least) to read carefully and catch any differences between them. |
6th option?: an optimization function that will perform a JUMP instead of a CALL if it is able to be validated that the code jump cannot access anything outside of the called function. This would limit functions that make external calls from having advanced memory access while allowing safe internal calls to reduced their gas count. This will also incentivize isolation/reduction of external calls to improve gas costs. |
This optimization means that source code using |
@haydenadams No? IMO, the current behaviour of this code is highly non-intuitive, and will make a lot of common operations much more difficult. @public
def getCaller() -> address:
return msg.sender
@public
def alsoGetCaller() -> address:
return self.getCaller() # always returns the contract's own address! |
Whatever address calls a function is msg.sender within the scope of that function. That is the only rule right now. Its not a massive leap to realize that extends to "on the internet nobody knows you're a dog" -> "on Ethereum nobody knows you're ̶a̶ ̶s̶m̶a̶r̶t̶ ̶c̶o̶n̶t̶r̶a̶c̶t̶ yourself" With account abstraction the divide between contracts and accounts will go from negligible to nonexistent. Contracts will get more complicated but also more versatile. Its pretty important that smart contract devs understand Ethereum account structure. I would prioritize simplicity of logic over what feels immediately intuitive here. Totally understand where you're coming from though! This seems reasonable, and personally I like it: @private
def checkOwner(_sender: address):
assert _sender == self.owner # <- replaced msg.sender with _sender
@public
def writeValue(_value: int128):
self.checkOwner(msg.sender) # <- pass msg.sender as an argument
self.value = _value |
We briefly discussed this in the bi-weekly meeting yesterday, but left it as unresolved. As it's quite a significant change, we have to study all the options carefully ;) |
@jacqueswww I certainly agree. |
@haydenadams it's straightforward to make small changes to my little example to make it work, but it's always going to be confusing. Here's an other summation: IF: a contract can call its own If the Vyper team decides to move towards the Finally, from the Auditability principle:
Outside of project contributors, requiring this level of nuance is far from simple for anyone reading code. |
I agree that it's confusing that |
2.) Investigate using DELETEGATECALL -> I was under the impression that this also resets the VM. Actually, I think you are right! I tested with this code in remix: library Lib {
function foo(uint a) returns(uint) { }
}
contract B {
using Lib for uint;
function bar() returns(uint){
uint a = 1;
return a.foo();
}
} If you call |
I like this. It gets rid of most of the confusion, and they're both useless in private functions anyway. |
@haydenadams I think "useless" might be an oversimplification, but it forces a specific design behavior that ultimately is very practical in reducing your attack surface. Basically, private functions should do very specific internal behaviors, preferably dealing only with internal state. |
To be clear, I'm in favor of either but not both. Preference for #2. |
I am also leaning towards delegate call. But definitely feel we might need a more gas efficient dispatcher+memory stack post beta. |
More gas efficient dispatcher can be done without breaking expectations if we make the right move now. Which one makes it easier to implement gas optimization on the dispatcher? |
If switching to delegatecall mean |
To better understand and make a decision here, it will be helpful if the VIP will include discussion of which stage of the compilation process we are changing. Also a discussion of use cases at that stage of compilation would help me. Also, if you can give a brief overview of the compilation strategy for newcomers this would help them join the discussion. |
@fulldecent and everyone: Looking again, I think it makes sense to reduce the scope of this VIP. A better title might be "Cost and usability Improvements to internal function calls". 👍/ 👎? Code reuse in general, (ie. enabling imports and something like inheritance/composability) is probably another much needed VIP, or perhaps a few. |
Or "New feature: call a function without using EVM CALL" |
I think this is modification to existing behavior, not a new feature proposal. We already have a composibility VIP I believe, as well as one for importing interfaces |
In the mid to longer term, I can imagine an EIP to significantly reducing the cost of a contract to CALL or DELEGATECALL its own address might be well received. |
I would vote against such an EIP. Call-to-self has extremely limited utility. |
I believe the issue is that even calls to @fubuloubu's response earlier in the thread:
seems like something worth pursuing. |
|
|
Will's Plan 2 -- The Expensive Reusability ManifestoThis plan is extremely expensive in gas. It is useful only for academic purposes. However it allows code reusability and very easy formal auditing.
To be clear, this is joke plan. It illustrates why we should proceed with Will's Plan 1. |
@fulldecent there is no need to do it like the above. One can just use the first 20 bytes of the internal calldata to send through the Also we can make it even more effecient, by only passing the data through if |
I think this whole discussion also highlights the fact that we should start doing some example gas cost comparisons to solidity. @ben-kaufman @fubuloubu @haydenadams @haydenadams What example contracts would you recommend? ERC20 & ERC721 seem like good options? |
If you can provide me with an all-best-practices-followed example Vyper project repository (including test cases) then I can get my team to implement 721 for this experiment! |
Starting with a proof of concept of using a jump table, just so you guys know if you don't see me. 👅 |
"Where'd Jacques go?" "Oh yeah, he's jumping tables now" |
@jacqueswww What was the decision here? You mentioned |
@haydenadams currently I am only working on 1.) Using jumps to improve gas efficiency of internal calls. I decided this was a worthy task, as unfortunately smart contract development is very gas-efficiency dependant at this stage, at the benefits would be quite significant. {tl;dr: 2.) has not been fully decided yet.} With regards to FWIW I do think following solidity here might be the safest option, it is really confusing for developers coming from solidity to have msg.sender be something different. Likewise a first time user coming from vyper, and going to solidity will be confused. |
I can support copying Solidity behavior. I can also support using words from Yellow Paper -- ORIGIN, CALLER. |
|
Ha @maurelian! You are such a sniper, I was waiting until the EIP was merged as a draft. So basically this EIP resulted directly from discussing #901, having the EIP accepted will both benefit solidity and vyper :) |
@fulldecent you expressed opposition to a hypothetical gas cost reductions on calls to self. The community would benefit from hearing your concerns here. |
@maurelian Thank you for the ping. I have reviewed and added all my comments to https://ethereum-magicians.org/t/eip-1380-reduced-gas-cost-for-call-to-self/1242/3 |
Preamble
Simple Summary
Abstract
In order to call a within the same contract function, Vyper uses the
CALL
opcode, which send a new message to call functions within the same contract. This has some nice safety benefits.Unfortunately it will result in developers using anti-patterns to save on gas, or access
msg.sender
in a function call.Motivation
The benefit of using
CALL
to access code in the same contract is to create a new execution context, with no risk of side effects from memory access. I admit that I find this to be quite an elegant use of the EVM for safety.Problems with using
CALL
Unfortunately, there are two major issues with the use of the
CALL
opcode for calling functions within the contract:1. Environment values change unexpectedly
At least two important environment opcodes will return different values:
CALLER
(ie.msg.sender
) andCALLVALUE
(ie.msg.value
). This complicates the use of functions for permission checking. The following is a naive vyper translation of the common "ownable" pattern for a simple storage solidity contract.2. Gas costs disincentivizes code reuse
The
CALL
opcode costs 700 gas (+ input data costs + function dispatching again). By contrast Solidity usesJUMP
(2 gas) to call a function.This imposes a large penalty on code reuse. Regardless Vyper's design philosophy favoring safety over gas efficiency, developers will respond to this incentive by simply copying and pasting boilerplate code. This is dangerous, and difficult to audit.
Specification:
I'm sorry, I don't have one specification. Below are some options I see for mitigating these issues.
From the Motivation section above, we have 3 parameters along which to analyze approaches to calling a function in the EVM.
1. Use
DELEGATECALL
instead ofCALL
CALLER
andCALLVALUE
)Seems like a decent solution!
3. Memory Safety: BAD (executes the code in the same memory context, nullifying reasons for using~I proposed this earlier, but I don't think this is a good solution. ~CALL
in the first place)2. Just use
JUMP
like SolidityCALLER
andCALLVALUE
)This is worth considering.
See also #330.
3. Implement a safer analog to Solidity's modifiers
These could have significant restrictions on them, like no access to
MSTORE
orSSTORE
, and not accepting arguments. I assume this would be done by withJUMP
, or just inlining the same code each time it's needed.CALLER
andCALLVALUE
)4. Create special variables which persist across message calls
Built-in vars like
caller
andcallvalue
can be created, which are magically passed to a function in a message call.My code above would thus be:
This feels a bit funky to me. Probably would be hard to implement safely, and obscures the true functioning of the EVM.
5. Status Quo
My problem in the code sample above can be addressed by passing the values I need as arguments:
Maybe that's OK? But it requires a deeper understanding of how both the EVM and Vyper work.
Backwards Compatibility
Dependent on approach.
Copyright
Copyright and related rights waived via CC0
The text was updated successfully, but these errors were encountered: