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

New Opcode ASYNC_CALL #118

Open
wanderer opened this Issue Jun 19, 2016 · 16 comments

Comments

Projects
None yet
7 participants
@wanderer
Copy link
Member

wanderer commented Jun 19, 2016

DESCRIPTION

If block.number >= METROPOLIS_FORK_BLKNUM, then opcode 0xfa functions equivalently to a CALL, except it takes 6 arguments not including value, and makes an ASYNC calls the child. No Items are PUSHED on to the stack. Normal Calls made after an Async call work as normal.

In the current execution model, async calls are executed after the parent execution is finished. If more than one Async calls (ac₀, ac₁ .... acₙ) happened during a given execution then after the parent execution has stopped then the async calls are ran in the order that they were generated (ac₀, ac₁ .... acₙ).

Here is a Sequence Diagram of the current model
current

The first instance of A (A₀) Make a call to contract B which then make a call back to A which creates a new instance of A (A₁)

Here is how Async calls would look
async

Only one instance of A is every created. All calls are run sequential.

In the future we maybe able to loosen the restriction on running calls sequentially. In a Concurrent Model Async calls could run in parallel. It is assumed that contracts running concurrently would be running at the same "speed" (gas Per clock cycles).

concurrent

REVERTS

Reverting also work that same way as the current model. If the parent execution run out of gas the child async calls will not be executed. (although a future improvement could this restriction optional)

GAS PRICE

The gas price should be half that of CALL (20 gas) since you could view an async call as one half of a sync call.

RATIONALE

By using async calls a contract programmer can guarantees about the state of storage and contract re-entry.

The async call can also be used in the future for cross shard communications. In this scenario async call would have to be modified to accept a port (which parent shard to call). It would work similar to ETHLOG except there would be no need for a get log.

Last async calls can be used to recall a contract if recursive calling is disable (in an actor model where contracts are actors or It can also be thought of as contracts being singleton instances). See here

REFERENCES

ALTERNATIVES

  • static calls #116
  • sandboxed calls #117
@Souptacular

This comment has been minimized.

Copy link
Member

Souptacular commented Jun 19, 2016

+1 Nice approach.

@kumavis

This comment has been minimized.

Copy link
Member

kumavis commented Jun 19, 2016

I think this is a fairly clean change. I was just imagining doing this at the solidity level, but more cleanly makes for a nice opcode.

Worth note that this does not handle the case of re-entrancy when you need a return value from the call

A couple things that should be explicitly stated in the async case:

  • value of msg.sender? (i imagine its same as normal)
  • value of tx.sender? (i imagine its same as normal)

The gas price should be half that of CALL (20 gas) since you could view an async call as one half of a sync call.

seems like the change in computational cost is minimal. you get to free up some memory bc the call stack is empty but we dont set the gasfee of CALL's by stack depth anyways. gas price should be approximately the same as a standard CALL

The async call can also be used in the future for cross shard communications.

since the params are different I think it will be easiest to just define a new opcode when we're ready for cross-shard comms

curious if theres a way to modify this without muddying it up such that you can get return values as well

async.series([
 doX,
 doY,
], getResults)
@wanderer

This comment has been minimized.

Copy link
Member Author

wanderer commented Jun 19, 2016

curious if theres a way to modify this without muddying it up such that you can get return values as well

async.series([
 doX,
 doY,
], getResults)

Yeah I really want to build up to being able to do something like this. I image async call going into a queue (FIFO). So if contract A was running and contract B & C both made async calls to it there would be 2 message in contract A's message queue. In this proposal once A is done running the next message in the queue is ran.

Now to facilitate callbacks we could give the contract access to the queue. So instead of the kernel shifting the queue the contract it self could do it. I image something like SHIFT_QUEUE which when called could load the next message into memory at a given index. You then could build event loop style programming which would allow for pattern you gave an example of

since the params are different I think it will be easiest to just define a new opcode when we're ready for cross-shard comms

It would need one extra parameter but it would essentially work the same. Ideally there won't be explicit shards. Just ports that contract sends message throught that allow it to communicate to sub-contracts or parent contracts.

seems like the change in computational cost is minimal. you get to free up some memory bc the call stack is empty but we dont set the gasfee of CALL's by stack depth anyways. gas price should be approximately the same as a standard CALL

I think you right but If we introduce a concurrency model (add that has to happen with sharding at some level) the difference will grow

A couple things that should be explicitly stated in the async case:
value of msg.sender? (i imagine its same as normal)
value of tx.sender? (i imagine its same as normal)

yep they will remain the same.

@kumavis

This comment has been minimized.

Copy link
Member

kumavis commented Jun 19, 2016

I think you right but If we introduce a concurrency model (add that has to happen with sharding at some level) the difference will grow

all the more reason to let the cross-shard async call just be a different opcode and not worry about defining that now

@wanderer

This comment has been minimized.

Copy link
Member Author

wanderer commented Jun 19, 2016

all the more reason to let the cross-shard async call just be a different opcode and not worry about defining that now

yep, lets cross that bridge when we get there. But where I'm coming from, all shards are invisible to the contract.

@taoeffect

This comment has been minimized.

Copy link

taoeffect commented Jun 22, 2016

Sorry, I am rather ignorant about the low-level details of the EVM's opcodes and execution model, but over in the solidity github I was wondering whether the message-passing Actor model from Erlang (and/or the improved Agent model from Clojure), could solve all of the reentrancy issues.

After chatting in the solidity gitter I got the impression that it can. So my question is whether this info is at all relevant or useful or related to (in any way) to the creation of such an ASYNC_CALL opcode?

@PeterBorah

This comment has been minimized.

Copy link

PeterBorah commented Jun 23, 2016

I think this is a fairly clean change. I was just imagining doing this at the solidity level, but more cleanly makes for a nice opcode.

I'm not inherently opposed to this, if it's useful, but I don't think it's hard to get the requested functionality with the existing opcodes. There is nothing saying that you must make an EVM call at the moment your high-level language has the word call. Keep a queue in memory, and when you reach the end of your execution, send the messages in the queue.

@wanderer

This comment has been minimized.

Copy link
Member Author

wanderer commented Jun 23, 2016

@PeterBorah you can almost get the same functionality as current. But if you build the queue inside the contract the gas costs will be off since the first call to the contract will have to pay for the anyother calls that may be in the queue. Likewise if we had strictly actor model approach you could accomplish the multiple instance writing to the same storage by breaking storage off into a contract it self.

So its more about "what is the sanest default" I would like to try to argue here that a singleton/actor model for contracts make more sense. Using only async calls contracts would act like a singleton instances.

@taoeffect yes exactly this is the first step needed to move fully to a actor model/ singleton instance for contracts

@PeterBorah

This comment has been minimized.

Copy link

PeterBorah commented Jun 23, 2016

But if you build the queue inside the contract the gas costs will be off since the first call to the contract will have to pay for the anyother calls that may be in the queue.

I don't think I understand the distinction you're trying to draw. In the current model, each call pays for itself and any subcalls it creates. I believe that the same will be true in an async model, except that in the async model all the calls are bunched at the end rather being interspersed throughout the code. Can you give me an example where the gas would be different?

@wanderer

This comment has been minimized.

Copy link
Member Author

wanderer commented Jun 23, 2016

@PeterBorah lets walk through an example and see if we can figure out there is a misunderstand in either of our knowledge.

  1. a tx is sent to contract A

  2. contract A async calls contract B with 100 gas (call 0)

  3. contract A async calls contract B with 200 gas (call 1)

  4. contract A async calls contract B with 300 gas (call 2)

  5. contract A finishes running.

  6. Now contract B will run call 0 and it call contract A (call 3) and finishes

  7. Now contract B will run call 1 and it call contract A (call 4) and finishes

  8. Now contract B will run call 2 and it call contract A (call 5) and finishes

  9. Now contract B will run call 3 and finishes

  10. Now contract B will run call 4 and and finishes

  11. Now contract B will run call 5 and finishes

  12. the Tx finishes execution

Now how would you duplicate that functionality currently? I have several answers in mind but I'm curious what you will come up with.

@PeterBorah

This comment has been minimized.

Copy link

PeterBorah commented Jun 23, 2016

That's not how I understood the original suggestion. Given this line:

async calls are executed after the parent execution is finished

I would expect it to go like this:

  1. a tx is sent to contract A

  2. contract A async calls contract B with 100 gas (call 0)

  3. contract A async calls contract B with 200 gas (call 1)

  4. contract A async calls contract B with 300 gas (call 2)

  5. contract A finishes running.

  6. Now contract B will run call 0 and it call contract A (call 3) and finishes

  7. Now contract A will run call 3 and finishes

  8. Now contract B will run call 1 and it call contract A (call 4) and finishes

(etc.)

That's how it will work if we're just moving execution of calls to the end of the parent execution. And if you model that in the normal EVM, the gas semantics will be the same, since you're still making the calls from the right context, just at a different time.

If instead all calls are going into a global FIFO queue, then you need all contracts to be aware of that queue. Modeled on the current EVM, that would look like:

  1. A tx is sent to the queue contract, with a message for contract A (call 0a)
  2. The queue contract runs call 0a which causes it to call contract A (call 0b)
  3. Contract A runs call 0b and calls the queue contract with a message for contract B (call 1a)
  4. The queue contract receives the message, stores it, and immediately finishes.
  5. Contract A continues call 0b and calls the queue contract with another message for contract B (call 2a)
  6. The queue contract receives the message, stores it, and immediately finishes.
  7. Contract A finishes call 0b.
  8. The queue contract gets back execution (because it's still in call 0a) and picks up the next message, which was sent in 1a. It calls contract B (call 1b.)
    (etc.)

This does indeed have different gas semantics, since calls don't need to pay for their subcalls. You could model the old semantics by having the queue contract keep track of the tree of calls, and refuse to pass more gas than its parent had available.

@gcolvin

This comment has been minimized.

Copy link
Collaborator

gcolvin commented Jul 22, 2016

In the pure actor model each actor has a FIFO queue which allows multiple writers and one reader. So contract A sends a message to contract B and it goes on contract B's queue and A keeps running. Eventually A's message gets to B and B runs. B can asynchronously send messages to A and keep on running itself. I haven't thought about a global queue, though it seems it would be a bottleneck. I haven't thought about gas. I have wondered about synchronous calls, which I think require a second, higher priority queue to (mostly) maintain present semantics for present calls. @wanderer @PeterBorah

@wanderer

This comment has been minimized.

Copy link
Member Author

wanderer commented Jul 23, 2016

@PeterBorah

That's how it will work if we're just moving execution of calls to the end of the parent execution. And if you model that in the normal EVM, the gas semantics will be the same, since you're still making the calls from the right context, just at a different time.

Yep that is almost correct... There are a few edges cases when putting all the calls at the end.

  1. a - async ->b then b-call->a. b would have a reentry here if async was just a call at the end of the execution

  2. more memory would be required. Esp if you conditional used async, where the conditions changed on reentry. Also more complex memory management in the contract would be required to account for additional reentries.

One of the additional goals here is to eventually move to a concurrent model, then asyncs could run in parallel. The other goal would be to eventually move to a pure actor model.

@gcolvin

In the pure actor model each actor has a FIFO queue which allows multiple writers and one reader. So contract A sends a message to contract B and it goes on contract B's queue and A keeps running

I have wondered about synchronous calls, which I think require a second, higher priority queue to (mostly) maintain present semantics for present calls

For now calls would have to act exactly the same I think. But it would be really nice to able to move to a pure actor model. Unfortunately some contracts actually rely on reentry. I think there is a way around this. But we need to get this eip in first.

@gcolvin

This comment has been minimized.

Copy link
Collaborator

gcolvin commented Jul 24, 2016

So here is a possible Actor model for ASYNC_CALL as I see it right now.

  • For a contract (actor) to receive a message from within Ethereum or from without is the same - it starts a transaction.
  • A transaction is the atomic execution of contract code.
  • Within a transaction contract code can CALL other contracts - these calls are not explicitly part of the actor model, but are part of the contract code execution.
  • Within a transaction contract code can create other contracts (other actors) - creating actors is part of the actor model.
  • Within a transaction contract code can ASYNC_CALL (send messages to) other contracts - this is of course the heart of the actor model.
  • Within a transaction a contract cannot receive messages, they get queued - this is part of the actor model as well.

This model ensures that contract calls within transactions remain distinct from messages in the Actor model, and since calls already occur only within transactions the other restrictions on Actors come for free. No call for receiving messages is needed - this is simply the entry point of the contract.

Note that the current CALL opcodes allow for recursion. The #116, #117, and #119 proposals aim to prevent some of the dangers this implies.

@gcolvin

This comment has been minimized.

Copy link
Collaborator

gcolvin commented Jul 24, 2016

Some background I dug up on the Actor Model. A few references:

As the third paper puts it: (broken into bullet points by me)

  • A computational system in the Actor Model, called a configuration, consists of a collection of concurrently executing actors and a collection of messages in transit.
  • Each actor has a unique name (the uniqueness property) and a behavior, and communicates with other actors via asynchronous messages.
  • Actors are reactive in nature, i.e. they execute only in response to messages received. An actor’s behavior is deterministic in that its response to a message is uniquely determined by the message contents.
  • Message delivery in the Actor Model is fair. The delivery of a message can only be delayed for a finite but unbounded amount of time.
  • An actor can perform three basic actions on receiving a message...
  1. create a finite number of actors with universally fresh names,
  2. send a finite number of messages, and
  3. assume a new behavior.

Assuming a new behavior can be as simple as storing a value that affects a branch in the next execution of the contract, or as fancy as creating a new contract with generated code and storing its address as the new behavior.

@gcolvin

This comment has been minimized.

Copy link
Collaborator

gcolvin commented Jul 24, 2016

I'd especially appreciate one of our mathematicians explaining Agha's typed pi calculus for dummies like me.

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