Stateful vs. Stateless Blockchain Contracts
Or what is wrong with Ethereum Contracts and how the new Ardor Lightweight Contracts can fix this
To understand how broken the existing Ethereum smart contracts are, take a look at one of their simplest examples, scroll down a bit and review the innocent looking 30 lines of code representing the MyToken contract example.
This is the sort of example from which every new smart contracts developer will start from, straight from the horse’s mouth, ethereum.org.
So what can be so wrong with such a harmless looking example? Let’s dissect the operation of the contract and try to identify problems.
Ethereum Contracts are Stateful
The balanceOf array on line #8 is the mother of all evil (computer sciencely speaking). This array maintains the balances of the accounts holding MyToken, in programming jargon we call this balanceOf array the “state” of the contract, since this information is maintained between invocations of the contract and is stored on the blockchain.
What is wrong with this approach is that it is a magnet for concurrency problems and various exotic attack vectors.
For example, take a look at the transfer() method on line #23 and think what would happen if the transfer method is invoked by two different threads, both withdrawing all the balance of the same message sender into another account, each running on its own CPU at the same time.
If on one CPU a thread passes the balance check on line #24 but did not yet reach the balance deduction on line #26, while another thread running on another CPU, invokes transfer from the same sender at the same time, both threads will pass the check on line #24, allowing the message sender to double spend his tokens i.e. transfer up to twice his available balance.
We call this problem a race condition and any program maintaining shared state like the balanceOf array suffers from it.
What this means, is that Ethereum contracts cannot execute in parallel. Therefore, even if you have one of those monsters on your desk, your contracts still execute on only one of the CPUs leaving the whole server mostly idle. Ideally what we want when we process a block full of contract invocation transactions, is to run all contracts in parallel but this is impossible because of the shared state.
Having shared state is causing other problems, for example, what if between lines #26 and #27 the contract developer decides to invoke an external contract? This external contract may throw exception leaving the sender without his balance even though this balance was not yet credited to the recipient.
Even worse, such external contract invocation may maliciously invoke the transfer method again from the external contract sending the tokens to itself. The following resource confusingly names this problem a “race condition” even though it has nothing to do with concurrency, and defines some best practices to follow to avoid these problems. However, as contracts become more complex and mission critical, these best practices become very difficult to enforce.
To summarize, relying on the contract to save its state is a bad idea, it opens up several attack vectors and manipulation options and also causes constant risk of locking funds in a contract because of difficult to find bugs.
Ardor Lightweight Contracts to the rescue
Ardor, being based on NXT technology, already has several ways to save state securely on the blockchain, using entities like messages, account properties, aliases, cloud data, various types of tokens and more. All these entities are built into the core blockchain using their own transaction types and APIs.
Based on this existing functionality, we designed our lightweight contracts to be stateless. Meaning that the contract itself never saves any state like the balanceOf array. Instead all state modifications required by the contract are submitted as normal transactions, which once accepted into the blockchain, will modify some of the entities listed above in order to save information into the blockchain. Later, when the same or another lightweight contract requires some state information like a token balance or account KYC information, it will use one of the public APIs to query it from the blockchain.
A direct 1:1 example of a lightweight contract similar to the above Ethereum contract is not feasible since in Ardor all you need in order to change token balance is invoke the transferAsset API, so let’s look into another example to see how this works.
This simplistic Hello World example demonstrates how an Ardor lightweight contract can modify the state of the blockchain by sending a message transaction without keeping any internal state.
Since there is no shared data stored in the HelloWorld contract, it is obvious that many HelloWorld contract instances can execute in parallel without causing any concurrency related problems or Ethereum like race conditions.
Any transactions submitted by the contract will be handled by the blockchain built in race condition prevention thus off-loading the responsibility for handing this difficult problem from the contract developers to the core blockchain developers.
Since lightweight contracts are executed by a single node not by all nodes, there is no risk of flooding the blockchain with duplicate messaging transactions. As you can see the contract is invoked, does some processing, submits some transactions and quits. The next invocation of the contract does not rely on any data saved into the contract itself so can run in parallel.
We believe that the stateless approach to contract execution used by Ardor will prove safer and more scalable compared to other stateful approaches.