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

Propose RETURNDATACOPY and RETURNDATASIZE. #211

Merged
merged 15 commits into from Dec 1, 2017

Conversation

@chriseth
Contributor

chriseth commented Feb 13, 2017

Copy of summary:

A mechanism to allow returning arbitrary-length data inside the EVM has been requested for quite a while now. Existing proposals always had very intricate problems associated with charging gas. This proposal solves the same problem while at the same time, it has a very simple gas charging mechanism and reqires minimal changes to the call opcodes. Its workings are very similar to the way calldata is handled already: After a call, return data is kept inside a virtual buffer from which the caller can copy it (or parts thereof) into memory. At the next call, the buffer is overwritten. This mechanism is 100% backwards compatible.

@Arachnid

This comment has been minimized.

Show comment
Hide comment
@Arachnid

Arachnid Feb 13, 2017

Collaborator

👍

Collaborator

Arachnid commented Feb 13, 2017

👍

@pirapira pirapira referenced this pull request Feb 13, 2017

Closed

Byzantium changes #229

12 of 12 tasks complete
@chfast

This comment has been minimized.

Show comment
Hide comment
@chfast

chfast Feb 16, 2017

Contributor
  1. With this change transaction processing can allocate ½M words of memory + ½M words of return data of gas cost of ½M + (½M)²/512 + ½M + (½M)²/512 = M + M²/1024. Previously allocating M words of memory was M + M²/512. It is not possible to keep more than one returndata buffer around, right?
  2. The identity precompiled contract (aka memcpy) will require 2 copies (from input buffer to return buffer, from return buffer to destination). It was designed around the idea that implementations are able to do single copy.
Contributor

chfast commented Feb 16, 2017

  1. With this change transaction processing can allocate ½M words of memory + ½M words of return data of gas cost of ½M + (½M)²/512 + ½M + (½M)²/512 = M + M²/1024. Previously allocating M words of memory was M + M²/512. It is not possible to keep more than one returndata buffer around, right?
  2. The identity precompiled contract (aka memcpy) will require 2 copies (from input buffer to return buffer, from return buffer to destination). It was designed around the idea that implementations are able to do single copy.
@Arachnid

This comment has been minimized.

Show comment
Hide comment
@Arachnid

Arachnid Feb 16, 2017

Collaborator

With this change transaction processing can allocate ½M words of memory + ½M words of return data of gas cost of ½M + (½M)²/512 + ½M + (½M)²/512 = M + M²/1024.

The peak memory consumption is no higher, though - all that memory is allocated under the current system during the second call's lifetime.

It is not possible to keep more than one returndata buffer around, right?

Correct. Effectively, this just keeps around the memory (or part thereof) of subcalls for longer.

The identity precompiled contract (aka memcpy) will require 2 copies (from input buffer to return buffer, from return buffer to destination). It was designed around the idea that implementations are able to do single copy.

Good point.

Collaborator

Arachnid commented Feb 16, 2017

With this change transaction processing can allocate ½M words of memory + ½M words of return data of gas cost of ½M + (½M)²/512 + ½M + (½M)²/512 = M + M²/1024.

The peak memory consumption is no higher, though - all that memory is allocated under the current system during the second call's lifetime.

It is not possible to keep more than one returndata buffer around, right?

Correct. Effectively, this just keeps around the memory (or part thereof) of subcalls for longer.

The identity precompiled contract (aka memcpy) will require 2 copies (from input buffer to return buffer, from return buffer to destination). It was designed around the idea that implementations are able to do single copy.

Good point.

@obscuren

This comment has been minimized.

Show comment
Hide comment
@obscuren

obscuren Feb 16, 2017

Member

👍

Member

obscuren commented Feb 16, 2017

👍

@gavofyork

This comment has been minimized.

Show comment
Hide comment
@gavofyork

gavofyork Feb 20, 2017

this doubles one of memory requirements and copying for returning data.

  • in the case where sub-call environment's MEMSIZE >> RETURNDATASIZE, then you would need to copy the data to a separate buffer in order to be able to discard the sub's memory;
  • in the case that the sub's MEMSIZE ~= RETURNDATASIZE, then you would want to keep sub's memory around to avoid the extra, potentially large, copy (memcpy built-in is a particularly notable case of this).

gas pricing (an implementations) would either have to have heuristics for both policies, or would be rather inefficient in one or the other situation.

either way, gas pricing will have to change.

gavofyork commented Feb 20, 2017

this doubles one of memory requirements and copying for returning data.

  • in the case where sub-call environment's MEMSIZE >> RETURNDATASIZE, then you would need to copy the data to a separate buffer in order to be able to discard the sub's memory;
  • in the case that the sub's MEMSIZE ~= RETURNDATASIZE, then you would want to keep sub's memory around to avoid the extra, potentially large, copy (memcpy built-in is a particularly notable case of this).

gas pricing (an implementations) would either have to have heuristics for both policies, or would be rather inefficient in one or the other situation.

either way, gas pricing will have to change.

@chriseth

This comment has been minimized.

Show comment
Hide comment
@chriseth

chriseth Feb 20, 2017

Contributor

Space-complexity is always much easier to handle than time-complexity: We can easily compute an upper bound on the required memory given a block gas limit, while it is not that easy to come up with a max import time for a block. Because of that, I would opt to gauge the gas costs assuming the memory of the callee is kept alive (in practices, this of course translates to a recommended memory size for a node given the current block gas limit).

Anyway, discarding the callee's memory might be a good thing if we are low on memory, but in general, the gas costs do not pay for "memory * time" but only for memory, so it should not make a difference asymptotically.

Contributor

chriseth commented Feb 20, 2017

Space-complexity is always much easier to handle than time-complexity: We can easily compute an upper bound on the required memory given a block gas limit, while it is not that easy to come up with a max import time for a block. Because of that, I would opt to gauge the gas costs assuming the memory of the callee is kept alive (in practices, this of course translates to a recommended memory size for a node given the current block gas limit).

Anyway, discarding the callee's memory might be a good thing if we are low on memory, but in general, the gas costs do not pay for "memory * time" but only for memory, so it should not make a difference asymptotically.

@gavofyork

This comment has been minimized.

Show comment
Hide comment
@gavofyork

gavofyork Mar 3, 2017

Note: to preserve existing peak-memory characteristics, it might be reasonable to define any memory-resizing operation as clearing the RETURNDATA return buffer.

gavofyork commented Mar 3, 2017

Note: to preserve existing peak-memory characteristics, it might be reasonable to define any memory-resizing operation as clearing the RETURNDATA return buffer.

@chfast

This comment has been minimized.

Show comment
Hide comment
@chfast

chfast Mar 3, 2017

Contributor

What is the semantic of both instructions when the return buffer was never assigned or has been cleared already?

Contributor

chfast commented Mar 3, 2017

What is the semantic of both instructions when the return buffer was never assigned or has been cleared already?

@chriseth

This comment has been minimized.

Show comment
Hide comment
@chriseth

chriseth Mar 3, 2017

Contributor

Never assigned should clearly be empty, and I guess it should be the same for the other case. @gavofyork could you explain the reasoning behind clearing a bit, please?

Contributor

chriseth commented Mar 3, 2017

Never assigned should clearly be empty, and I guess it should be the same for the other case. @gavofyork could you explain the reasoning behind clearing a bit, please?

Show outdated Hide outdated EIPS/returndatacopy.md

@arkpar arkpar referenced this pull request Mar 9, 2017

Closed

Byzantium release #4833

12 of 12 tasks complete
@holiman

This comment has been minimized.

Show comment
Hide comment
@holiman

holiman Mar 19, 2017

Contributor

Does this apply only to CALL, or also DELEGATECALL, CALLCODE, PURE_CALL, STATIC_CALL?

And which cases would 'clear' the returndata-menory? The same ones that it applies to ? How about if I have my data in my returndata buffer, then do CREATE , which in itself can do CALLs ? Would it be cleared?

Contributor

holiman commented Mar 19, 2017

Does this apply only to CALL, or also DELEGATECALL, CALLCODE, PURE_CALL, STATIC_CALL?

And which cases would 'clear' the returndata-menory? The same ones that it applies to ? How about if I have my data in my returndata buffer, then do CREATE , which in itself can do CALLs ? Would it be cleared?

@axic

This comment has been minimized.

Show comment
Hide comment
@axic

axic Mar 19, 2017

Member

Does this apply only to CALL, or also DELEGATECALL, CALLCODE, PURE_CALL, STATIC_CALL?

To all of them I would assume.

And which cases would 'clear' the returndata-menory? The same ones that it applies to ?

My understanding was each subsequent opcode which writes to it resets it.

How about if I have my data in my returndata buffer, then do CREATE, which in itself can do CALLs ? Would it be cleared?

I assume there is a "return data buffer" for each instance, e.g. the caller of CREATE has a "return data buffer", whose contents is replaced by the RETURN emitted during CREATE. However, the execution of CREATE is an instance itself and therefore any inner CALLs will not affect the outside return buffer.

Member

axic commented Mar 19, 2017

Does this apply only to CALL, or also DELEGATECALL, CALLCODE, PURE_CALL, STATIC_CALL?

To all of them I would assume.

And which cases would 'clear' the returndata-menory? The same ones that it applies to ?

My understanding was each subsequent opcode which writes to it resets it.

How about if I have my data in my returndata buffer, then do CREATE, which in itself can do CALLs ? Would it be cleared?

I assume there is a "return data buffer" for each instance, e.g. the caller of CREATE has a "return data buffer", whose contents is replaced by the RETURN emitted during CREATE. However, the execution of CREATE is an instance itself and therefore any inner CALLs will not affect the outside return buffer.

@chriseth

This comment has been minimized.

Show comment
Hide comment
@chriseth

chriseth Mar 20, 2017

Contributor

Any call-like opcode resets the buffer, even failed calls reset (even due to not enough funds or because the callee went out of gas).

To simplify the implementation, I would say that also create resets the buffer. So at any time, there is at most one non-empty return data buffer across all stack frames.

To summarize: Any opcode that attempts to create a new call stack frame resets the buffer of the current stack frame right before it executes, even if that opcode fails.

Contributor

chriseth commented Mar 20, 2017

Any call-like opcode resets the buffer, even failed calls reset (even due to not enough funds or because the callee went out of gas).

To simplify the implementation, I would say that also create resets the buffer. So at any time, there is at most one non-empty return data buffer across all stack frames.

To summarize: Any opcode that attempts to create a new call stack frame resets the buffer of the current stack frame right before it executes, even if that opcode fails.

@holiman

This comment has been minimized.

Show comment
Hide comment
@holiman

holiman Mar 21, 2017

Contributor

To simplify the implementation, I would say that also create resets the buffer. So at any time, there is at most one non-empty return data buffer across all stack frames.

I like that, and I think it maybe should be clarified in the EIP proposal.

Contributor

holiman commented Mar 21, 2017

To simplify the implementation, I would say that also create resets the buffer. So at any time, there is at most one non-empty return data buffer across all stack frames.

I like that, and I think it maybe should be clarified in the EIP proposal.

pirapira added a commit to pirapira/yellowpaper that referenced this pull request Mar 22, 2017

@arkpar

This comment has been minimized.

Show comment
Hide comment
@arkpar

arkpar Apr 7, 2017

Never assigned should clearly be empty, and I guess it should be the same for the other case. @gavofyork could you explain the reasoning behind clearing a bit, please?

if A calls B which allocates 1MB (and returns some portion of it), then, afterwards, A, in some independent memory-resizing operation, extends memory to be 1MB before calling RETURNDATACOPY over some portion of the existing 1MB, then the peak allocation is 2MB; prior to this PR it is only 1MB. Peak usage is only preserved if there are no memory resizing operations prior to RETURNDATACOPY. (it might be reasonable to define any memory-resizing operation as clearing the RETURNDATA return buffer)

arkpar commented Apr 7, 2017

Never assigned should clearly be empty, and I guess it should be the same for the other case. @gavofyork could you explain the reasoning behind clearing a bit, please?

if A calls B which allocates 1MB (and returns some portion of it), then, afterwards, A, in some independent memory-resizing operation, extends memory to be 1MB before calling RETURNDATACOPY over some portion of the existing 1MB, then the peak allocation is 2MB; prior to this PR it is only 1MB. Peak usage is only preserved if there are no memory resizing operations prior to RETURNDATACOPY. (it might be reasonable to define any memory-resizing operation as clearing the RETURNDATA return buffer)

@arkpar

This comment has been minimized.

Show comment
Hide comment
@arkpar

arkpar Apr 7, 2017

Summary of discussing the above issue with @chriseth:

We’ve considered a few possible cases for an implementation that keeps around all of the memory allocated by the callee available for the caller.

  1. Before the EIP. Contract A calls into B. B allocates 10kb and returns 1kb. After that A has 1kb allocated and allocates additional 8kb. Peak memory consumption of 11kb is reached when B returns.
  2. EIP Implemented as currently specified. Contract A calls into B. B allocates 10kb and returns 1kb. A allocates additional 8kb. A calls RETURNDATACOPY to copy 1kb to a previously unallocated area. Peak memory consumption is 19kb at at the end of the sequence.
  3. EIP with @gavofyork’s additional requirement to clear return data on memory expansion. Contract A calls into B. B allocates 10kb and returns 1kb. A is now forced to get the return data before doing any additional allocations. A calls RETURNDATACOPY to copy 1kb to a previously unallocated area. A allocates additional 8kb which clears the return data. Peak consumption remains at 11kb

There should be a clarification to @gavofyork’s proposal that if memory expansion happens to be caused by RETURNDATACOPY return data is cleared after the instruction completes a copy.

arkpar commented Apr 7, 2017

Summary of discussing the above issue with @chriseth:

We’ve considered a few possible cases for an implementation that keeps around all of the memory allocated by the callee available for the caller.

  1. Before the EIP. Contract A calls into B. B allocates 10kb and returns 1kb. After that A has 1kb allocated and allocates additional 8kb. Peak memory consumption of 11kb is reached when B returns.
  2. EIP Implemented as currently specified. Contract A calls into B. B allocates 10kb and returns 1kb. A allocates additional 8kb. A calls RETURNDATACOPY to copy 1kb to a previously unallocated area. Peak memory consumption is 19kb at at the end of the sequence.
  3. EIP with @gavofyork’s additional requirement to clear return data on memory expansion. Contract A calls into B. B allocates 10kb and returns 1kb. A is now forced to get the return data before doing any additional allocations. A calls RETURNDATACOPY to copy 1kb to a previously unallocated area. A allocates additional 8kb which clears the return data. Peak consumption remains at 11kb

There should be a clarification to @gavofyork’s proposal that if memory expansion happens to be caused by RETURNDATACOPY return data is cleared after the instruction completes a copy.

@chriseth

This comment has been minimized.

Show comment
Hide comment
@chriseth

chriseth Apr 10, 2017

Contributor

After some more thought, it looks like the peak memory consumption is actually not a problem. Some extract of the following text should probably be added to the EIP itself at some point.

Let me try to formalize this a bit so we know what we are talking about. The promise of the evm is as follows: For any "reasonable" implementation I of the evm and any amount of gas g there is a number m so that any execution that starts with gas limit g inside I does not allocate more than m bytes of memory.

Of course all of this has to be taken with a grain of salt. In particular, "reasonable" codifies the tradeoff between implementation complexity and runtime performance.

As far as protocol changes are concerned, you can come up with a function m(g) which models the max memory allocation for a certain amount of start gas g for current implementations. Protocol changes should not cause big changes in this function.

In the specific example above, we consider a certain contract and notice that its memory consumption changes with the protocol change. But what we have to consider instead is the function that maps gas to max memory consumption.

So we have situation X: A uses 0 memory, then calls B, B allocates 10kb and returns 1kb. A allocates 8kb after the contract returns and then accesses return data.
This allocates 11kb before the change and 19kb after the change with roughly the same gas.

Now consider situation Y: A first allocates 9kb memory, then calls B, B allocates 10kb and returns. A accesses return data.

This should consume roughly the same gas as X but requires 19kb of memory before and after the change. So there is a contract that uses the same gas before and after the change but its max allocation is the same before and after the change and it is equal to the first example's max allocation after the change. Because of that, the first example is not a counter example.

I think this can be generalized: For any contract execution E there is a modified contract execution E' that allocates exactly the memory required in the current call frame as the first thing it does and then continues on exactly as E. Note that this is an existential statement, so it does not require a constructive way to come up with that value (the fact that it is cumbersome to come up with this constructive way is exactly what this EIP wants to fix). The gas required by E' should be the same as E (there is an issue with forwarding gas to callees and getting refunds, but I don't think this is a substantial issue as far as memory consumption is concerned), but the max memory consumption of E' should be the same as in the case where we keep the full callee memory around until the next call-like opcode.

Actual memory consumption due to fragmentation and contiguous memory might still be an issue, but I'm not sure if it makes any difference here.

Contributor

chriseth commented Apr 10, 2017

After some more thought, it looks like the peak memory consumption is actually not a problem. Some extract of the following text should probably be added to the EIP itself at some point.

Let me try to formalize this a bit so we know what we are talking about. The promise of the evm is as follows: For any "reasonable" implementation I of the evm and any amount of gas g there is a number m so that any execution that starts with gas limit g inside I does not allocate more than m bytes of memory.

Of course all of this has to be taken with a grain of salt. In particular, "reasonable" codifies the tradeoff between implementation complexity and runtime performance.

As far as protocol changes are concerned, you can come up with a function m(g) which models the max memory allocation for a certain amount of start gas g for current implementations. Protocol changes should not cause big changes in this function.

In the specific example above, we consider a certain contract and notice that its memory consumption changes with the protocol change. But what we have to consider instead is the function that maps gas to max memory consumption.

So we have situation X: A uses 0 memory, then calls B, B allocates 10kb and returns 1kb. A allocates 8kb after the contract returns and then accesses return data.
This allocates 11kb before the change and 19kb after the change with roughly the same gas.

Now consider situation Y: A first allocates 9kb memory, then calls B, B allocates 10kb and returns. A accesses return data.

This should consume roughly the same gas as X but requires 19kb of memory before and after the change. So there is a contract that uses the same gas before and after the change but its max allocation is the same before and after the change and it is equal to the first example's max allocation after the change. Because of that, the first example is not a counter example.

I think this can be generalized: For any contract execution E there is a modified contract execution E' that allocates exactly the memory required in the current call frame as the first thing it does and then continues on exactly as E. Note that this is an existential statement, so it does not require a constructive way to come up with that value (the fact that it is cumbersome to come up with this constructive way is exactly what this EIP wants to fix). The gas required by E' should be the same as E (there is an issue with forwarding gas to callees and getting refunds, but I don't think this is a substantial issue as far as memory consumption is concerned), but the max memory consumption of E' should be the same as in the case where we keep the full callee memory around until the next call-like opcode.

Actual memory consumption due to fragmentation and contiguous memory might still be an issue, but I'm not sure if it makes any difference here.

@Arachnid

This comment has been minimized.

Show comment
Hide comment
@Arachnid

Arachnid Apr 10, 2017

Collaborator

Although I agree with @chriseth's reasoning that the max memory per gas remains the same either way, I think that clearing like this as another advantage: it would allow EVM implementations to treat the memory as a single contiguous block of virtual memory for the entire transaction. Each time a contract calls another, the new contract's memory starts off at the end of the previous contract's memory, just like how stack allocation works in languages like C.

This is currently possible, but the EIP as originally proposed would require each contract to have its own memory buffer(s) instead. With @gavofyork 's proposed variation, this would again be a possible optimisation.

Collaborator

Arachnid commented Apr 10, 2017

Although I agree with @chriseth's reasoning that the max memory per gas remains the same either way, I think that clearing like this as another advantage: it would allow EVM implementations to treat the memory as a single contiguous block of virtual memory for the entire transaction. Each time a contract calls another, the new contract's memory starts off at the end of the previous contract's memory, just like how stack allocation works in languages like C.

This is currently possible, but the EIP as originally proposed would require each contract to have its own memory buffer(s) instead. With @gavofyork 's proposed variation, this would again be a possible optimisation.

@Arachnid

This comment has been minimized.

Show comment
Hide comment
@Arachnid

Arachnid Apr 10, 2017

Collaborator

I would also like to make the recommendation that a RETURNDATACOPY whose bounds extend past the end of return data should be defined as an exceptional condition, causing the executing contract to immediately return with status 1 and no return data to the caller.

There's no sensible reason to copy past the end of return data, and we should treat it as the error it almost certainly is, rather than silently filling with zeroes.

Collaborator

Arachnid commented Apr 10, 2017

I would also like to make the recommendation that a RETURNDATACOPY whose bounds extend past the end of return data should be defined as an exceptional condition, causing the executing contract to immediately return with status 1 and no return data to the caller.

There's no sensible reason to copy past the end of return data, and we should treat it as the error it almost certainly is, rather than silently filling with zeroes.

@chfast

This comment has been minimized.

Show comment
Hide comment
@chfast

chfast Apr 10, 2017

Contributor

In the @arkpar's example (2), after the call to B, A can keep only 1 kB of memory by coping the return buffer. Then the memory peek would stay 11 kB. Trade-offs as usually...

Contributor

chfast commented Apr 10, 2017

In the @arkpar's example (2), after the call to B, A can keep only 1 kB of memory by coping the return buffer. Then the memory peek would stay 11 kB. Trade-offs as usually...

pirapira added some commits Nov 17, 2017

Show outdated Hide outdated EIPS/eip-211.md

I moved the file. I still need to read it again to approve.

pirapira added a commit that referenced this pull request Nov 20, 2017

Show outdated Hide outdated EIPS/eip-211.md
Show outdated Hide outdated EIPS/eip-211.md
Show outdated Hide outdated EIPS/eip-211.md

pirapira added some commits Nov 20, 2017

Show outdated Hide outdated EIPS/eip-211.md
@pirapira

Looks good to me.

@pirapira pirapira merged commit dde2fe5 into master Dec 1, 2017

@pirapira pirapira deleted the returndatacopy branch Dec 1, 2017

@axic

This comment has been minimized.

Show comment
Hide comment
@axic

axic Jan 17, 2018

Member

For reference, eWASM was proposing the same with ewasm/design#12

Member

axic commented Jan 17, 2018

For reference, eWASM was proposing the same with ewasm/design#12

@axic axic referenced this pull request Jan 17, 2018

Closed

Arbitrary call return sizes #12

pirapira added a commit to pirapira/yellowpaper that referenced this pull request Jan 19, 2018

pirapira added a commit to pirapira/yellowpaper that referenced this pull request Jan 19, 2018

pirapira added a commit to pirapira/yellowpaper that referenced this pull request Jan 19, 2018

@Silur Silur referenced this pull request Feb 25, 2018

Merged

EVM byzantium opcodes #149

@aerth aerth referenced this pull request Jun 5, 2018

Merged

HF7 @ 36050 #24

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment