Call_context

Intro

Call_context is used to track the correct call context that the memory, storage and stack ops are applied to.

Data elements

struct CallContext {
    msg_sender: EthAddress
    gas_available: u256
    gas_used_before_this_call: u256
    // tells us if this transaction will throw or revert in the future 
    is_persistant: bool
    is_static_call: bool
    // a count of the number of storage updates that need to be undone. 
    revert_todo: u256
    // This is the global counter minimum global counter that needs to be reverted.
    gc_to_revert_to: u256
    call_depth: u256
}

impl Commitable for CallContext {
    fn commit(){
        return (
            msg_sender + 
            gas_available * r + 
            gas_used_before_this_call * r**2 +
            is_persistant * r**3 +
            is_static_call * r**4 +
            revert_todo * r**5 +
            gc_to_revert_to * r**6 +
            call_depth * r**7
        )
    }
}

The call_context is a random linear combination of

  1. msg.sender
  2. gas_available
  3. gas_used_before_this_call
  4. is_persistant
  5. is_static_call
  6. revert_todo
  7. gc_to_revert_to
  8. last_call_context

r = hash(msg.sender .. last_call_context)

\(call\_context = msg.sender + gas\_available * r .. last\_call\_context *r ^5\)

Terms

is_presistant

Is a binary flag that tells us if this transaction will throw or revert in the future

revert_todo

Is a count of the number of storage updates that need to be undone.

gc_to_revert_to

This is the global counter minimum global counter that needs to be reverted.

Reverts/throws

When we reach reverts/throws we need to do two things

  1. Find how far back we need to revert
  2. revert all storage operations until that point

When we reach op REVERT, it is actually a special error case we should not only do above two things, but also refund left gas (other errors consume all left gas). See Q4 for all other errors.
han

How far do we revert

  1. If gas_passed = gas available the two transactions are in the same revert_context and both will be reverted together.

TODO: Confirm this is all we need to check how far to revert ?

Storage

When we are applying storage updates if the is_persitant flag = 1 nothing happens. We apply them as is.

If it is_persistant == 0 we need to preapare to revert every storage write we do. This is done by counting every storage update that is done. We excute all sotrage update as they are defined in the smart contract. When we get to the point where the revert happens we undo all these updates.

We basically force the prover to show us todo_revert storage writes that happened between gc and gc_to_revert_to .

To make this easier storage write bus mapping will contain the value before it was written and the value that was written. For each element in this list we set that storage value to the value_before for todo_revert elements with gc < = gc_to_revert_to

Questions:

  1. Does static call throw on state writes or does it let you make writes as long as they are not persistant ? Assuming it does not let you make writes if you do it throws. Would be good to confirm this

    In STATICCALL, it reverts to snapshot and consumes all gas_available when:

    1. op are able to modify state (SSTORE, LOGX, CREATEX)
    2. CALL with value
      Reference
  2. How does reverts get stopped does it just revert the current call or can it revert others ?

    It reverts others too. See this example. The parent delegate calls to child, who later revert a call to grandchild. The child's revert reverts the grandchild's state but not the parent's.

  3. How does throw get stopped ? Does it just revert everything until there is a tx with more gas left ?

    throw is just revert without keeping gas left, which is due to compiler's behavior (and throw is removed in solidity 0.5), they both use op REVERT in the end.
    When any errors happen, it always reverts to the snapshot before CALL, DELEGATECALL, STATICCALL, and only error due to REVERT will keep the gas_available, others just consume all given gas.

  4. Should storage be seperated by only address instead of call cotext?

    If we do multiple call to a child contract and change its storage, we need their storage to be continue, for example, contract A tries to trigger B.counter++ multiple times, if we seperated them by different call context, the result won't be correct.

    Yes
    barryWhiteHat

  5. If call context is only for seperatation (or namespacing) in state circuit, why not just use the gc at CALL?

    In the call context now, I can see only gas_used_before_this_call and gc_to_revert_to are unique, others are easily to collide with other calls. For example, I can have two identical call to same child contract.

    Call context is also used as a digest of parent call and last child call, so all the things we need to memorize should be inside call context.

  6. When a call is going to end, we have an awkward situation to handle: callee doesn't explicitly halt (opcodes doesn't ended with STOP, RETURN, REVERT), then evm will pad it with an explicit STOP.

    In each op_lookup(addr, pc, op) (not sure if it's the function signature in you idea), we need prover to explicitly tell us if we should do the lookup or not. If we do, we lookup for correct op, otherwise we know it's padded STOP and switch to parent context.

Select a repo