Conditionally cancelling part of a dapp's state changes

in #ethereum8 years ago

(This article was first published on wemakethings -- by me, of course.)

Intro (history -- skip if in a hurry)

I've recently been busy writing a pygments lexer for Solidity. This has been an unusual learning process. Instead of getting familiar with known tropes for a language, I got a view of what's possible and how it's done on a lower level.

For example, here's a little (as of yet untested) trick to keep multiple references to the same object in a mapping. (Relevant: post on references and mappings by Peter Vessenes, published on the same day that example was written.)

Anyway, it also occured to me yesterday that another under-used feature of the language could allow conditionally "reversing" undesired state changes that occured during dapp execution.

This has implications. Consider a dapp that modifies the state (changes its own globals, makes send()s and call()s), but inspects the state after execution, and resets to a previous state if the results are unacceptable.

This is not specific to Solidity. In fact, reverting state changes upon unsuccessful termination for the failed call only is the default mode of operation for the EVM. Solidity doesn't have try-throw-catch yet, just throw, so the compiler wraps every external call in a structure that makes the parent fail if the child failed, -- to
stay on the safe side. I applaud those who had the foresight.

Also, did you know? The guaranteed failure of a throw is achieved by JUMPing to an invalid destination.

external is a keyword (primer -- skip if acquainted)

The under-used feature mentioned before is a function visibility specifier, external. It marks a function as "to be used by other dapps, not this one". From the docs:

The expression this.g(8); is also a valid function call, but this time, the function will be called “externally”, via a message call and not directly via jumps. Functions of other contracts have to be called externally. For an external call, all function arguments have to be copied to memory.

(The docs currently lack an example use case.)

Why isn't it used much?

Probably because functions are public by default; meaning their signatures are available for other dapps to use, and external calls is the only way dapps do it. Making function F external instead of public in dapp D prevents the developer of D from making direct calls to it (via JUMPs on assembly level), but the outside developer can
still D.F() just the same. external limits the developer of D, but not everybody else, so its usefulness is not self-evident.

An "external call" manifests on the blockchain as an "internal transaction" (counter-intuitive phrasing, yes) from account A to account B, with gas (and possibly value) attached. EVM-wise, arguments are pushed to memory, "depth counter" is incremented, and control is transferred. If execution succeeds, return values are pushed to memory, and control is returned (together with all remaining gas). If execution fails, all performed state changes are rolled back, and control is returned (remaining gas, if any, is consumed).

Application (the trick -- read and understand)

By making an external call to itself, a dapp could have an isolated part that either executes as expected, or not at all. "As expected", of course, is loaded, since the expectations need to be specified somewhere.

Here's a proof of concept (also on GitHub, or pygmentized). Details after the code.

ThisExternalAssembly.sol:

contract ThisExternalAssembly {
    uint public numcalls;
    uint public numcallsinternal;
    uint public numfails;
    uint public numsuccesses;
    
    address owner;

    event logCall(uint indexed _numcalls, uint indexed _numcallsinternal);
    
    modifier onlyOwner { if (msg.sender != owner) throw; _ }
    modifier onlyThis { if (msg.sender != address(this)) throw; _ }

    // constructor
    function ThisExternalAssembly() {
        owner = msg.sender;
    }

    function failSend() external onlyThis returns (bool) {
        // storage change + nested external call
        numcallsinternal++;
        owner.send(42);

        // placeholder for state checks
        if (true) throw;

        // never happens in this case
        return true;
    }
    
    function doCall(uint _gas) onlyOwner {
        numcalls++;

        address addr = address(this);
        bytes4 sig = bytes4(sha3("failSend()"));

        bool ret;

        // work around `solc` safeguards for throws in external calls
        // https://ethereum.stackexchange.com/questions/6354/
        assembly {
            let x := mload(0x40) // read "empty memory" pointer
            mstore(x,sig)

            ret := call(
                _gas, // gas amount
                addr, // recipient account
                0,    // value (no need to pass)
                x,    // input start location
                0x4,  // input size - just the sig
                x,    // output start location
                0x1)  // output size (bool - 1 byte)

            //ret := mload(x) // no return value ever written :/
            mstore(0x40,add(x,0x4)) // just in case, roll the tape
        }

        if (ret) { numsuccesses++; }
        else { numfails++; }

        // mostly helps with function identification if disassembled
        logCall(numcalls, numcallsinternal);
    }

    // will clean-up :)
    function selfDestruct() onlyOwner { selfdestruct(owner); }
    
    function() { throw; }
}

Here, doCall() is just a wrapper to work around Solidity's security mechanism, whereas failSend() demonstrates utility of the approach.

Note first that it can only be called externally due to the external specifier; and it can't be called by other dapps because of the onlyThis modifier. In effect, it can only be called by the same contract, via an external call.

The fuction body is all placeholders to increase readability. In a "real" compratmentalised call, one would first perform checks, preferably via modifiers; then changes to contract storage; then nested external calls. That is, the check-change-send routine.

After that, one would perform "state checks" and look for unexpected state changes, for all accounts touched by this function. If there are any discrepancies, changes resulting from all nested calls can be
reverted -- at the cost of gas passed to the call. The rest of the contract can continue execution.

This is far from fool-proof. Re-entrancy checks would still be required, along with everything else we have and haven't yet come up with. But it demonstrates a feature of Solidity not yet available, namely exception handling -- admittedly, in a convoluted manner.

It's alive! (testing -- read and verify)

To demonstrate the technique, I've deployed the dapp on the ETH main-net (see etherscan.io or live.ether.camp). There were a few failed iterations, and then a final working one on Morden test-net, but I couldn't get the block explorers to validate code there, so had to re-deploy the final on main-net again.

Set up some variables:

> var demoaddr = "0x6abd2b75ff5f306a4d99bfab1ff84b57bb9d23e7 ";
> var demoabi = <snipped>;
> var d = eth.contract(demoabi).at(demoaddr);

Storage variables after creation:

> console.log(d.numcalls(), d.numcallsinternal(), d.numfails(), d.numsuccesses())
> 0 0 0 0

Run doCall() and check variables:

> var lasttx = d.doCall(50000, {from: owneraddr, value: 42, gas: 200000})
>
> console.log(d.numcalls(), d.numcallsinternal(), d.numfails(), d.numsuccesses())
> 1 0 1 0

As expected, doCall() threw, so 42 wei remained with the dapp.

Donate (please do)

Decided to try Steemit to see if I could get rewarded for all this work. Other ways are good, too.

Research into this exact issue, dapp testing and writing the article took approximately 12 hours.

Sort:  

I've seen the dapp-a-day on reddit, and like it a lot. It's pretty hard to find the good stuff by "poking around", there's just too much to browse. It gets even harder to follow the feeds if one's got AFK obligations.

Thanks for linking tryExceptElse.sol, it's much closer to C++/C#/Java than what I present here, and exactly the language trope I refer to in the intro. May come handy if I ever get to writing something that's actually useful.

Following you!

Congratulations @veox! You have received a personal award!

2 Years on Steemit
Click on the badge to view your Board of Honor.

Do you like SteemitBoard's project? Then Vote for its witness and get one more award!

Congratulations @veox! You received a personal award!

Happy Birthday! - You are on the Steem blockchain for 3 years!

You can view your badges on your Steem Board and compare to others on the Steem Ranking

Vote for @Steemitboard as a witness to get one more award and increased upvotes!