Conditionally cancelling part of a dapp's state changes
(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 JUMP
ing 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 JUMP
s 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.
Very nice! You might be interested in these:
https://gist.github.com/nmushegian/de35e6a389b12da4f9bc1a828b44c5a6
https://steemit.com/ethereum/@nikolai/dapp-a-day-2-base-actor
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.
Congratulations @veox! You received a personal award!
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!