Stateless Smart Contracts

The stateless smart contract or dumb contract is a design pattern used to drastically reduce the gas cost of Ethereum smart contracts.

A stateless design comes in two parts:

  1. The Dumb Contract
  2. The Filter

Dumb Contract

Here is how you might write a standard datastore contract:

contract DataStore {
mapping(address => mapping(bytes32 => string)) public store;

event Save(address indexed from, bytes32 indexed key, string value);

function save(bytes32 key, string value) {
store[msg.sender][key] = value;
Save(msg.sender, key, value);
}
}

Let’s say we want to save the following key / value pair using the contract above:

key: “ethereum”
value: “Ethereum is a decentralized platform that runs smart contracts: applications that run exactly as programmed without any possibility of downtime, censorship, fraud or third party interference.”

This transaction will cost 181,181 gas (or 89,797 gas if we use IPFS).

If we follow a stateless design our contract would look like this:

contract DataStore {
function save(bytes32 key, string value) {}
}

That’s it. We don’t store any contract state and we don’t fire off events. We implement the rest of the logic off-chain. If we use the same data as above the transaction costs 35,721 gas (or 25,841 gas if we use IPFS) which is an 80% reduction in gas cost.

So if we aren’t saving any contract state how do we access our data?

Take a look at this transaction on Etherscan, scroll down to Input Data, and click the Convert To Ascii button. Our data lives in the input of the transaction.

Filter

A Filter processes the transactions of a dumb contract and provides an interpretation of that data.

In a standard dapp we would interact with a smart contract like this:

Frontend => Web3 => Ethereum Network => Web3 => Frontend

In a stateless design we do the following:

Frontend => Web3 => Ethereum Network => Backend => Frontend

When a user interacts with our dumb contract from the frontend (using something like MetaMask) we watch for incoming transactions on the backend and process them.

For example, using the information in the transaction above, we can implement the logic from the standard version of the datastore contract off-chain.

We can use InputDataDecoder to recover information from the transaction inputs:

const abi = [
{
constant: false,
inputs: [
{ name: "key", type: "bytes32" },
{ name: "value", type: "string" }
],
name: "save",
outputs: [],
payable: false,
type: "function"
}
];
const decoder = new InputDataDecoder(abi);
const decodeInput = input => decoder.decodeData(input);

We need to do a little extra processing to recover our bytes32 key arg:

const processArgs = input =>
input.inputs.map((arg, i) => {
const type = input.types[i];
if (type === "string") {
return arg;
}
if (type === "bytes32") {
const toHex = `0x${arg.toString("hex")}`;
return web3.toUtf8(toHex);
}
return arg;
});

Tying it all together:

const run = async () => {
const tx = "0xc9fdf51d...";
const transaction = await web3.eth.getTransaction(tx);
const input = decodeInput(transaction.input);
if (input.name === "save") {
const args = processArgs(input);
const address = transaction.from;
const key = args[0];
const value = args[1];
// save the address / key / value to a database
}
};

We can now save the key / value under the address that the transaction was sent from in a database that provides a better user experience.

Extras Details

You can apply various levels of statelessness to your contract depending on your needs. For example, you can store usernames in a registry to enable other smart contracts to identify users.

function registerUsername(bytes32 username) external {
var hasUsername = usernames[msg.sender];
var isOwned = addresses[username];
if (isOwned != 0) throw; // prevents registration of existing username
if (hasUsername != 0) throw; // prevents registered address from registering another username
if (!isLowercase(username)) throw; // username must be lowercase
usernames[msg.sender] = username;
addresses[username] = msg.sender;
}

Meanwhile, you can keep other content completely stateless.

function post(string data) external {
var username = usernames[msg.sender];
if (username == 0) throw; // user must be registered
}

Tampering

Users can easily validate whether or not a Filter provider is tampering with content by looking at the inputs of transactions on the blockchain.

However, tampering can be positive! A Filter can enhance stateless content with metadata, media, links, and handles.

One of the great things about dumb contracts is that anyone can build a Filter on top of one and provide their own interpretation of the data. If you are worried about censorship you can also open source your Filter to allow others to spin up your interpretation.

Trade-Offs

A stateless design is only useful for certain types of projects. Keep these trade-offs in mind:

  1. A stateless design requires a mix of decentralization and centralization to provide the best user experience / gas cost.
  2. Other contracts cannot access your data.
  3. If your contract has no events you must processes every block to find relevant transactions. This can be mitigated by firing off empty events with marginal extra gas costs.

That’s it, I hope you found this educational!


A fantastic decentralized social media product now exists that builds on top of the ideas presented in this article: Peepeth.