Ethereum is the OP_EVAL of cryptocurrency
I’m a bitcoin person, but I’ve been paying more and more attention to Ethereum over the past few weeks. I have some Ether, but I’m still probably an outsider looking in when it comes to Ethereum.
I’ll start by saying I like aspects of Ethereum. I think they’ve improved upon certain things bitcoin was having trouble with. And while it has its downsides, the one that I speak most positively about is the fact that they’ve done away with UTXOs. Instead, they maintain account balances while dynamic behavior is moved into contracts. I’ve personally had a hell of a time trying to optimize the utxo set in my own bitcoin full node. It can be a real pain (bitcoin currently has 38 million utxos in 10 million different unspent transactions).
But while some of the features of Ethereum seemed attractive and inviting, there was one major component of Ethereum that always rubbed me the wrong way: how dynamic the contract language is. It’s a far cry from bitcoin’s scripting system.
Bitcoin Script History
Before we get into all that, some relevant info on the way I see things:
I’ve spent the past 7 months syncing the bitcoin blockchain repeatedly to test my full node. I’ve maybe used more of sipa’s bandwidth than anyone (sorry, sipa, your node is usually the fastest). I’ve seen bitcoin history replay in my terminal hundreds of times.
So it follows that I’ve also seen a lot of weird scripts. For example, I can tell you when the first p2sh script appeared on the blockchain. The weird part is, this script wasn’t executed as p2sh yet because p2sh only went into effect 23 days later.
I can tell you when OP_CODESEPARATOR first appeared on the blockchain, along with other non-push opcodes in the input script. The output script even has a OP_NOP2 OP_DROP: the same convention we now use for OP_CHECKLOCKTIMEVERIFY, and this was ages before CLTV. I regard that one as the coolest TX ever sent.
I can tell you when the SIGHASH_SINGLE bug was first used in tandem with FindAndDelete() on the testnet blockchain. That was first time valid signatures ever appeared in a scriptPubkey! It was long thought to be logically impossible (and it is, but sighash_single allows a way around that).
And finally, I can tell you that the reason I discovered all of these is because my scripting system was originally failing to execute them properly.
Because of this, I absolutely concede that the bitcoin scripting system has really really weird parts that are more dynamic than you’d expect. But still, it’s a pretty primitive little language. It’s pretty hard to write a script with holes in it as long as you understand how a stack works. And it is still in stark contrast to Ethereum’s contract language.
(Now, you might be expecting me to go into detail about which has prettier syntax. Unfortunately, that’s already been done by a similarly named blog post.)
The bitcoin scripting language is comprised of 116 opcodes (not including the 1–75 push opcodes). Most of these are not allowed to be used in any standard script type, and some are disabled completely. The 15 disabled opcodes opened up attack vectors that could be used to DOS the network with a single script, and this was a certainty (this included shifts, multiplication, division, and string manipulation). On the other hand, the opcodes not allowed in any standard script types were just considered “too weird” to be in output scripts, and they weren’t generally used anyway. Perhaps they opened up room for people to write less-than-foolproof scripts, but who knows? Most nodes won’t relay transactions containing non-standard script types, but they are allowed on the consensus layer still (the blockchain).
It wasn’t until late 2014 that the scripting language was opened up again: p2sh redeem scripts are now allowed to be non-standard scripts, with a catch: p2sh does not and has never allowed non-push opcodes in the input script, even on the blockchain.
So, all of this development fuss of disabling opcodes, considering only a few script types standard, and re-enabling non-standard scripts was over a primitive little scripting language that doesn’t have half the features of Ethereum.
A few days before the DAO got drained, I shared some of these thoughts about the scripting system with @mckie. I kept thinking that Ethereum was going to run into the same issues as the bitcoin scripting system: too dynamic, too many unknowns, too many ways to break. Except in the case of Ethereum, these problems would be amplified by the fact that it is more dynamic than bitcoin script ever was.
People are comparing the DAO incident to Satoshi’s int64 overflow bug back in 2010. I don’t think it is a logical comparison for a few reasons:
- No funds were stolen there.
- Satoshi didn’t blacklist an address or artificially reclaim funds.
- This was a bug involving an int64 overflow that Satoshi would have fixed anyway, hacker or no hacker.
- Satoshi didn’t tell exchanges to stop trading and single-handedly control the market.
- This was in 2010. There was a lot less at stake back then.
And finally, Satoshi did not rewrite history. He fixed a bug. Bitcoin users, including miners, upgraded and began to reject the blocks exploiting the bug. The bad chain was eventually reorg’d and overtaken by the new main chain.
There’s a much more obvious comparison to be made here…
Let’s talk about some other bitcoin opcodes. Not the disabled ones. Not the non-standard ones. How about the ones that were flat out removed from the codebase? There are the OP_VER opcodes, but there’s also one I find particularly relevant to the whole DAO situation.
OP_EVAL was the precursor to P2SH and was meant to replace OP_NOP1.
A regular p2sh execution looks something like:
Input: [sigs] [redeem-script]
Output: HASH160 [20-byte-hash] EQUAL
And the script interpreter implicitly executes the redeem script for you.
OP_EVAL was very similar, but a bit more explicit in that it would look like:
Input: [sigs] [redeem-script]
Output: DUP HASH160 [20-byte-hash] EQUALVERIFY EVAL
What OP_EVAL did was pop an item off the stack and interpret it as script code, thus giving almost the same behavior we now have with p2sh. But this can introduce some strange behavior. What if people used OP_EVAL in ways other than the standard method laid out above? What if you used OP_EVAL inside of a script currently being executed with OP_EVAL?
Yep, it could recurse. A script could invoke itself endlessly. Sound familiar?
Output: DUP HASH160 [20-byte-hash] EQUALVERIFY DUP EVAL
Redeem: DUP EVAL
And we now have a roundabout turing complete language.
As soon as this was discovered, a github issue was posted. Luckily, this was still two months before OP_EVAL was going to be activated.
This issue was actually anticipated by Gavin beforehand. A recursion limit (see also: gas price) had been implemented, but the recursion count was not being incremented properly. In the end, even with fixing the recursion count, there were still too many unknowns. OP_EVAL turned out to be a major failure and was scrapped entirely. It was replaced with the much safer pay-to-scripthash that we now know today.
Before the DAO was drained, a blog post was posted about how easy it is to mishandle values and create a recursive call bug in an Ethereum contract. There was a subsequent DAO Github PR posted. However, that pull request had a slightly different outcome than the bitcoin issue regarding a similar recursion issue: although it was merged, it didn’t seem to receive as much attention. And the merged code was not able to be deployed.
What I’d like to point out here is the different design and development philosophies in both currencies. Bitcoin is very conservative with changes. Not only does it move slow, it’s very hesitant to add any slick behavior in the first place. A lot of people dislike this slow movement. A lot of bitcoin users have actually moved over to Ethereum specifically because they’re fed up with a lack of deployments on certain fronts (one in particular that I won’t mention). Some companies are also upset about bitcoin’s lack of features and rate of change (see my snide remark above for a link).
I’m here to tell them that they’re not wrong for being sick of all the waiting, but they are wrong for thinking the extra time to wait has served no purpose. Segwit has been a pull request for months now, and it is still being reviewed. That is for very good reason.
While you might think bitcoin has slowly adopted this conservative approach only recently because it’s gotten so big, I urge you to read the OP_EVAL thread. Bitcoin had this mentality even back in 2011.
To emphasize this, this is what luke-jr wrote in the OP_EVAL thread:
IMO, new protocol-level features probably should have at a minimum 6 months — maybe a whole year — of testing/review, with ideally at least 3 or 4 competent developers signing off as having actually read the code and given it some thought.
My thoughts: I am protocol-conservative implementation-liberal by nature. I would prefer slow and steady with the protocol rather than moving hasty.
Bitcoin was lucky to have this disaster early on. And even luckier that it wasn’t deployed before this issue was discovered. Bitcoin learned from this incident and made the proper adjustments.
Ethereum on the other hand seems to be more cavalier with its design and its changes. It has no issues with doing a hard fork every so often. It has no issues with a high-level dynamic language dictating its contracts. Yes, it has pretty syntax, but how much is hidden underneath the surface when you’re using a high-level turing complete language?
I don’t think Ethereum is learning from history.
Dear Ethereum devs / contract devs / DAO devs, before you write a contract, do a soft fork, a hard fork, or roll a new currency, watch the bitcoin blockchain sync a few times. Look at the weird transactions, read up on the history of them. Find out which holes were plugged in bitcoin already. Learn from the mistakes, just like bitcoin did.