Executive summary
Lightweight contracts will never lock user funds when used correctly.
Introduction
Starting a large software development project is always associated with a lot of risk and uncertainty. When we started working on Ardor back in 2016 the risks were huge but we made it. Ardor was launched on mainnet by January 2018 as we promised with its first child chain Ignis implementing the features of the NXT blockchain and more. Ever since, we have been working on adding more functionality and utility to this amazing product.
A wise person once told me, when you need to deliver a large software project, instead of spending several month on planning, simply start it. The relevant knowledge and experience you’ll gain in a week will prove much more useful than months of planning. And so we did this with the lightweight contracts project. However, one thing we didn’t know how to solve upfront was how to prevent a contract from locking user funds in case the contract runner failed (or didn’t want) to run the contract. As we previously discussed, lightweight contracts are not part of the consensus, only contract runner nodes, which choose to do so, will run them. But then as a user sending funds to a contract, how can I be sure that the contract will execute and not lock my funds? Now, we have a solution.
You see, most smart contract frameworks don’t need to worry about locking user funds since contract running is part of the consensus. If a node does not run a contract when it should, the node is left on a fork. Historical fund locking incidents were a result of a bug in the contract itself, not in the execution framework. One notable example was the Ethereum Paritytech multisig contract, which was changed from a standalone status to a library when the devs forgot to remove the kill method.
But this approach of requiring every node to execute every contract is clearly not scalable. So people came up with alternative ideas like Plasma which describes a method to move contract execution to child chains. Don’t be surprised if this sounds familiar to what we do in Ardor, great minds think alike, but then the Plasma designers run into the same issue, if not everyone is required to run a contract, how do you make sure that someone will run it at all and not just lock your funds? The plasma whitepaper mentions a mass-exit idea, in case someone acts maliciously on some child chain everyone can escape from it. We think this is a bad idea as it is complex to implement and simple to manipulate. Instead, our approach is different: we do not really send the funds to the contract account until the contract successfully runs.
Solution — phasing by hashed secret
One great thing about Ardor is that we are building it on top of the rich functionality of NXT. We are not starting from scratch like so many of the other blockchain solutions. NXT already has a little known feature we call “phasing by hashed secret”. The idea is that a transaction is submitted together with a hash of a secret generated by a well known hash function. Balances (or other state changes) are only updated once the secret is revealed by a separate approval transaction. In case the secret is not revealed by a certain block height, the transaction remains in the blockchain but the balances are not updated. This functionality exists in NXT and has been running in production since 2015.
Now we can leverage this small gem using Lightweight Contracts. The client funding the contract will specify a hash of a secret only they know. The contract, running based on this trigger transaction, will submit its own transaction using the same “hashed secret”. Once the client reveals the secret, both transactions will be applied together as one atomic operation. If the client decides not to reveal the secret, both transactions will remain in the blockchain but their balance changes will be ignored. Therefore, if the contract fails to run, the client won’t reveal the secret so when the phasing height is reached, its funds will be released. Simple local solution, no need for mass-exit or the like.
Let’s see how this works in practice. The client defines an approval modal by a hash of a secret only it knows.
Figure 1 — Define the Hashed Secret
Now we can trigger a contract and phase the transaction by this secret hash
Figure 2 — Hashed Secret Attached when Sending a Transaction to a Contract
The contract submits its own transaction using phasing by the same secret hash. It doesn’t know the secret, it only echoes the same hash of a secret provided by the trigger transaction.
The client now reviews both transactions and approves them by revealing the hashed secret.
Figure 3 — Approve Both Transactions by Revealing the Phased Secret
As a result both transaction balance changes are applied on the same block without risk of locking or manipulation.
Summary
An up-front design challenge of lightweight contracts was the risk of locking user funds. This risk is now mitigated using the original NXT “Phasing by Hashed Secret” functionality built into the contract execution framework.